From 53f8030b8fa50bc024e7363a8ed68c96560a6066 Mon Sep 17 00:00:00 2001 From: Connell Date: Thu, 13 Feb 2020 11:02:35 -0800 Subject: [PATCH 01/45] JDK11 compatibility fixes --- build.gradle.kts | 2 +- .../InterceptionInstallerTests.java | 2 +- .../amazon/disco/agent/inject/Injector.java | 1 - .../disco/agent/inject/InjectorTests.java | 75 +++++++++++-------- 4 files changed, 44 insertions(+), 36 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1bdd8ed..9cd45da 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,7 +41,7 @@ subprojects { } dependencies { - testCompile("org.mockito", "mockito-core", "1.+") + testCompile("org.mockito", "mockito-core", "3.+") } configure { diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java index 63a4a46..4dd932f 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java @@ -29,7 +29,7 @@ public class InterceptionInstallerTests { public void testIgnoreMatcherMatchesJavaInternals() throws Exception { //random selections from each of the sun, com.sun and jdk namespaces //ForName is used to prevent warnings such as "warning: AbstractMultiResolutionImage is internal proprietary API and may be removed in a future release" - Assert.assertTrue(classMatches(Class.forName("sun.awt.image.AbstractMultiResolutionImage"))); + Assert.assertTrue(classMatches(Class.forName("sun.misc.Unsafe"))); Assert.assertTrue(classMatches(Class.forName("com.sun.awt.SecurityWarning"))); Assert.assertTrue(classMatches(Class.forName("jdk.nashorn.api.scripting.AbstractJSObject"))); } diff --git a/disco-java-agent/disco-java-agent-inject-api/src/main/java/software/amazon/disco/agent/inject/Injector.java b/disco-java-agent/disco-java-agent-inject-api/src/main/java/software/amazon/disco/agent/inject/Injector.java index d075392..051ccb4 100644 --- a/disco-java-agent/disco-java-agent-inject-api/src/main/java/software/amazon/disco/agent/inject/Injector.java +++ b/disco-java-agent/disco-java-agent-inject-api/src/main/java/software/amazon/disco/agent/inject/Injector.java @@ -101,7 +101,6 @@ public static JarFile addToSystemClasspath(Instrumentation instrumentation, File try { JarFile jar = new JarFile(jarFile); instrumentation.appendToSystemClassLoaderSearch(jar); - addURL(ClassLoader.getSystemClassLoader(), jarFile.toURI().toURL()); return jar; } catch (Throwable t){ //safely continue diff --git a/disco-java-agent/disco-java-agent-inject-api/src/test/java/software/amazon/disco/agent/inject/InjectorTests.java b/disco-java-agent/disco-java-agent-inject-api/src/test/java/software/amazon/disco/agent/inject/InjectorTests.java index 40b967c..b8356fb 100644 --- a/disco-java-agent/disco-java-agent-inject-api/src/test/java/software/amazon/disco/agent/inject/InjectorTests.java +++ b/disco-java-agent/disco-java-agent-inject-api/src/test/java/software/amazon/disco/agent/inject/InjectorTests.java @@ -16,24 +16,19 @@ package software.amazon.disco.agent.inject; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.mockito.Mockito; import java.io.File; import java.io.FileOutputStream; import java.lang.instrument.Instrumentation; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import java.util.jar.JarOutputStream; import java.util.zip.ZipEntry; @@ -42,9 +37,16 @@ public class InjectorTests { @ClassRule public static TemporaryFolder tempFolder = new TemporaryFolder(); + private static Instrumentation instrumentation; + + @BeforeClass + public static void beforeClass() { + instrumentation = Injector.createInstrumentation(); + } + @Test public void testLoadAgent() throws Exception { - File dummyJarFile = createJar("testLoadAgent"); + File dummyJarFile = createJar("testLoadAgent", true); //pre-check that the class is not on the bootstrap classloader try { Class.forName(PremainClass.class.getName(), true, null); @@ -63,31 +65,31 @@ public void testLoadAgent() throws Exception { @Test public void testAddToSystemClasspath() throws Exception { - File dummyJarFile = createJar("testAddToSystemClasspath"); - Instrumentation instrumentation = Mockito.mock(Instrumentation.class); + final String name = "testAddToSystemClasspath"; + URL url; + url = ClassLoader.getSystemClassLoader().getResource(name); + Assert.assertNull(url); + File dummyJarFile = createJar(name, false); Injector.addToSystemClasspath(instrumentation, dummyJarFile); - Mockito.verify(instrumentation).appendToSystemClassLoaderSearch(Mockito.any()); - URL[] urlArray = ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs(); - List urlList = Arrays.asList(urlArray); - Set urls = new HashSet<>(urlList); - Assert.assertTrue(urls.contains(dummyJarFile.toURI().toURL())); + url = ClassLoader.getSystemClassLoader().getResource(name); + Assert.assertNotNull(url); } @Test public void testAddToBootstrapClasspath() throws Exception { - File dummyJarFile = createJar("testAddToBootstrapClasspath"); - Instrumentation instrumentation = Mockito.mock(Instrumentation.class); - Injector.addToBootstrapClasspath(instrumentation, dummyJarFile); - + final String name = "testAddToBootstrapClasspath"; + URL url; Class classFileLocator = Class.forName("net.bytebuddy.dynamic.ClassFileLocator$ForClassLoader"); Field bootLoaderProxyField = classFileLocator.getDeclaredField("BOOT_LOADER_PROXY"); bootLoaderProxyField.setAccessible(true); + URLClassLoader classLoader = (URLClassLoader) bootLoaderProxyField.get(null); - Method getURLs = URLClassLoader.class.getDeclaredMethod("getURLs"); - URL[] urls = (URL[])getURLs.invoke(bootLoaderProxyField.get(null)); - - Mockito.verify(instrumentation).appendToBootstrapClassLoaderSearch(Mockito.any()); - Assert.assertEquals(urls[0], dummyJarFile.toURI().toURL()); + url = classLoader.getResource(name); + Assert.assertNull(url); + File dummyJarFile = createJar(name, false); + Injector.addToBootstrapClasspath(instrumentation, dummyJarFile); + url = classLoader.getResource(name); + Assert.assertNotNull(url); } @Test @@ -111,21 +113,28 @@ public void addURL(URL url) { } } - private static File createJar(String name) throws Exception { + private static File createJar(String name, boolean withPremain) throws Exception { File file = tempFolder.newFile(name + ".jar"); try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { - //write a manifest specifying a premain class - jarOutputStream.putNextEntry(new ZipEntry("META-INF/")); - jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); - jarOutputStream.write(("Premain-Class: "+PremainClass.class.getName()+"\n\n").getBytes()); + //write a sentinal file with the same name as the jar, to test if it becomes readable by getResource. + jarOutputStream.putNextEntry(new ZipEntry(name)); + jarOutputStream.write("foobar".getBytes()); jarOutputStream.closeEntry(); - //write the PremainClass below, so that we can add it to bootstrap classloader for the loadAgentTest. - jarOutputStream.putNextEntry(new ZipEntry("software/amazon/disco/agent/inject/")); - jarOutputStream.putNextEntry(new ZipEntry("software/amazon/disco/agent/inject/InjectorTests$PremainClass.class")); - jarOutputStream.write(getBytes(PremainClass.class.getName())); - jarOutputStream.closeEntry(); + if (withPremain) { + //write a manifest specifying a premain class + jarOutputStream.putNextEntry(new ZipEntry("META-INF/")); + jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); + jarOutputStream.write(("Premain-Class: "+PremainClass.class.getName()+"\n\n").getBytes()); + jarOutputStream.closeEntry(); + + //write the PremainClass below, so that we can add it to bootstrap classloader for the loadAgentTest. + jarOutputStream.putNextEntry(new ZipEntry("software/amazon/disco/agent/inject/")); + jarOutputStream.putNextEntry(new ZipEntry("software/amazon/disco/agent/inject/InjectorTests$PremainClass.class")); + jarOutputStream.write(getBytes(PremainClass.class.getName())); + jarOutputStream.closeEntry(); + } } } return file; From dd25fcc43c6c6c26fd88c00c28e77bcc901f46d5 Mon Sep 17 00:00:00 2001 From: Connell Date: Thu, 27 Feb 2020 13:34:12 -0800 Subject: [PATCH 02/45] Remove reflective access to ForkJoinTask, as JDK11 deprecates unsafe accesses to JDK classes --- .../concurrent/ForkJoinPoolInterceptor.java | 3 +- .../concurrent/ForkJoinTaskInterceptor.java | 27 +++++++--- .../decorate/DecoratedForkJoinTask.java | 45 ++++++++--------- .../ForkJoinTaskInterceptorTests.java | 5 +- .../decorate/DecoratedForkJoinTaskTests.java | 50 ++++++------------- 5 files changed, 62 insertions(+), 68 deletions(-) diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ForkJoinPoolInterceptor.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ForkJoinPoolInterceptor.java index 1f3f918..22842db 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ForkJoinPoolInterceptor.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ForkJoinPoolInterceptor.java @@ -228,7 +228,8 @@ public static void onMethodEnter(@Advice.Argument(value = 0, typing = Assigner.T */ public static void methodEnter(Object task) { try { - DecoratedForkJoinTask.create(task); + DecoratedForkJoinTask.Accessor accessor = (DecoratedForkJoinTask.Accessor)task; + accessor.setDiscoDecoration(DecoratedForkJoinTask.create()); } catch (Exception e ) { log.error("DiSCo(Concurrency) could not propagate context into " + task); } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ForkJoinTaskInterceptor.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ForkJoinTaskInterceptor.java index af66637..dfce8fc 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ForkJoinTaskInterceptor.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ForkJoinTaskInterceptor.java @@ -15,6 +15,8 @@ package software.amazon.disco.agent.concurrent; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.implementation.FieldAccessor; import software.amazon.disco.agent.concurrent.decorate.DecoratedForkJoinTask; import software.amazon.disco.agent.interception.Installable; import software.amazon.disco.agent.logging.LogManager; @@ -29,6 +31,7 @@ import java.lang.reflect.Modifier; import static net.bytebuddy.matcher.ElementMatchers.*; +import static software.amazon.disco.agent.concurrent.decorate.DecoratedForkJoinTask.DISCO_DECORATION_FIELD_NAME; /** * ForkJoinPool and ForkJoinTask are a related pair of Java features for dispatching work to pools of threads @@ -59,7 +62,16 @@ public AgentBuilder install(AgentBuilder agentBuilder) { //for methods on ForkJoinTask, and elsewhere for methods on ForkJoinPool. .type(createForkJoinTaskTypeMatcher()) .transform((builder, typeDescription, classLoader, module) -> builder - .defineField(DecoratedForkJoinTask.DISCO_DECORATION_FIELD_NAME, DecoratedForkJoinTask.class, Modifier.PROTECTED) + .implement(DecoratedForkJoinTask.Accessor.class) + .defineField(DISCO_DECORATION_FIELD_NAME, DecoratedForkJoinTask.class, Modifier.PROTECTED) + + .defineMethod(DecoratedForkJoinTask.Accessor.GET_DISCO_DECORATION_METHOD_NAME, DecoratedForkJoinTask.class, Visibility.PUBLIC) + .intercept(FieldAccessor.ofField(DISCO_DECORATION_FIELD_NAME)) + + .defineMethod(DecoratedForkJoinTask.Accessor.SET_DISCO_DECORATION_METHOD_NAME, void.class, Visibility.PUBLIC). + withParameter(DecoratedForkJoinTask.class) + .intercept(FieldAccessor.ofField(DISCO_DECORATION_FIELD_NAME)) + .method(createForkMethodMatcher()) .intercept(Advice.to(ForkAdvice.class)) ) @@ -90,7 +102,8 @@ public static void onMethodEnter(@Advice.This Object thiz) { */ public static void methodEnter(Object task) { try { - DecoratedForkJoinTask.create(task); + DecoratedForkJoinTask.Accessor accessor = (DecoratedForkJoinTask.Accessor)task; + accessor.setDiscoDecoration(DecoratedForkJoinTask.create()); } catch (Exception e) { //swallow } @@ -117,10 +130,10 @@ public static void onMethodEnter(@Advice.This Object thiz) { */ public static void methodEnter(Object task) { try { - DecoratedForkJoinTask discoDecoration = DecoratedForkJoinTask.get(task); - discoDecoration.before(); + DecoratedForkJoinTask.Accessor accessor = (DecoratedForkJoinTask.Accessor)task; + accessor.getDiscoDecoration().before(); } catch (Exception e) { - log.error("DiSCo(Concurrency) unable to propagate context in ForkJoinTask " + task); + log.error("DiSCo(Concurrency) unable to propagate context in ForkJoinTask"); } } @@ -140,8 +153,8 @@ public static void onMethodExit(@Advice.This Object thiz) { */ public static void methodExit(Object task) { try { - DecoratedForkJoinTask discoDecoration = DecoratedForkJoinTask.get(task); - discoDecoration.after(); + DecoratedForkJoinTask.Accessor accessor = (DecoratedForkJoinTask.Accessor)task; + accessor.getDiscoDecoration().after(); } catch (Exception e) { //swallow } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/DecoratedForkJoinTask.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/DecoratedForkJoinTask.java index 6f46e62..9e4aee9 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/DecoratedForkJoinTask.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/DecoratedForkJoinTask.java @@ -16,8 +16,6 @@ package software.amazon.disco.agent.concurrent.decorate; -import java.lang.reflect.Field; - /** * ForkJoinTask is an abstract class with many public methods, so cannot enjoy the more natural decoration * treatment afforded to Runnables and Callables. Instead of 'decoration' in the strict sense, we - via instrumentation - @@ -27,35 +25,36 @@ public class DecoratedForkJoinTask extends Decorated { public static final String DISCO_DECORATION_FIELD_NAME = "$discoDecoration"; /** - * Create DiSCo propagation metadata on the supplied object, assumed to be a ForkJoinTask - * @param task the task to decorate - * @throws Exception reflection exceptions may be thrown + * Private constructor, use factory method for creation. */ - public static void create(Object task) throws Exception { - lookup().set(task, new DecoratedForkJoinTask()); + private DecoratedForkJoinTask() { } /** - * Retreive DiSCo propagation metadata from the supplied object, assumed to be a ForkJoinTask - * @param task the task from which to obtain the DiSCo propagation data - * @return an instance of a 'Decorated' - * @throws Exception relection exceptions may be thrown + * Create DiSCo propagation metadata for a ForkJoinTask + * @return a new instance of the DecoratedForkJoinTask object. */ - public static DecoratedForkJoinTask get(Object task) throws Exception { - return DecoratedForkJoinTask.class.cast(lookup().get(task)); + public static DecoratedForkJoinTask create() { + return new DecoratedForkJoinTask(); } /** - * Helper method to lookup the DiSCo decoration field, which was added to ForkJoinTask during interception - * by the treatment in ForkJoinTaskInterceptor - * @return a read/write Field representing the added DiSCo decoration field - * @throws Exception reflection exceptions may be thrown + * An interface we add to decorated ForkJoinTasks, to have bean get/set semantics on the added field. */ - static Field lookup() throws Exception { - //have to reflectively lookup the Decorated which exists inside the ForkJoinTask. - Class fjtClass = Class.forName("java.util.concurrent.ForkJoinTask", true, ClassLoader.getSystemClassLoader()); - Field decoratedField = fjtClass.getDeclaredField(DISCO_DECORATION_FIELD_NAME); - decoratedField.setAccessible(true); - return decoratedField; + public interface Accessor { + public static final String GET_DISCO_DECORATION_METHOD_NAME = "getDiscoDecoration"; + public static final String SET_DISCO_DECORATION_METHOD_NAME = "setDiscoDecoration"; + + /** + * Get the added discoDecoration field from an intercepted ForkJoinTask + * @return the discoDecoration field + */ + DecoratedForkJoinTask getDiscoDecoration(); + + /** + * Set the added discoDecoration field on an intercepted ForkJoinTask + * @param decoratedForkJoinTask the new value + */ + void setDiscoDecoration(DecoratedForkJoinTask decoratedForkJoinTask); } } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ForkJoinTaskInterceptorTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ForkJoinTaskInterceptorTests.java index 2e9f92a..373bbf3 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ForkJoinTaskInterceptorTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ForkJoinTaskInterceptorTests.java @@ -22,6 +22,7 @@ import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; +import software.amazon.disco.agent.concurrent.decorate.DecoratedForkJoinTask; import java.util.concurrent.CountedCompleter; import java.util.concurrent.ForkJoinTask; @@ -77,12 +78,12 @@ public void testForkAdviceSafe() { @Test public void testExecAdviceEnterSafe() { - ForkJoinTaskInterceptor.ExecAdvice.onMethodEnter(Mockito.mock(ForkJoinTask.class)); + ForkJoinTaskInterceptor.ExecAdvice.onMethodEnter(Mockito.mock(DecoratedForkJoinTask.class)); } @Test public void testExecAdviceExitSafe() { - ForkJoinTaskInterceptor.ExecAdvice.onMethodExit(Mockito.mock(ForkJoinTask.class)); + ForkJoinTaskInterceptor.ExecAdvice.onMethodExit(Mockito.mock(DecoratedForkJoinTask.class)); } @Test diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedForkJoinTaskTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedForkJoinTaskTests.java index 49e2976..9394c6f 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedForkJoinTaskTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedForkJoinTaskTests.java @@ -15,47 +15,27 @@ package software.amazon.disco.agent.concurrent.decorate; +import org.junit.Assert; import org.junit.Test; -import java.util.concurrent.ForkJoinTask; +import java.lang.reflect.Method; +import java.util.Arrays; -/** - * All tests can do is throw reflection errors, since unit-tests are agent-not-present scenario - */ public class DecoratedForkJoinTaskTests { - @Test(expected = NoSuchFieldException.class) - public void testCreateThrows() throws Exception { - ForkJoinTask fjt = new TestForkJoinTask(); - DecoratedForkJoinTask.create(fjt); - - } - - @Test(expected = NoSuchFieldException.class) - public void testGetThrows() throws Exception { - ForkJoinTask fjt = new TestForkJoinTask(); - DecoratedForkJoinTask.get(fjt); - + @Test + public void testCreate() { + DecoratedForkJoinTask decoratedForkJoinTask = DecoratedForkJoinTask.create(); + Assert.assertNotNull(decoratedForkJoinTask); } - @Test(expected = NoSuchFieldException.class) - public void testLookupThrows() throws Exception { - DecoratedForkJoinTask.lookup(); - } - - static class TestForkJoinTask extends ForkJoinTask { - @Override - public Object getRawResult() { - return null; - } - - @Override - protected void setRawResult(Object value) { - } - - @Override - protected boolean exec() { - return false; - } + @Test + public void testMethodNames() { + Method[] methods = DecoratedForkJoinTask.Accessor.class.getDeclaredMethods(); + Assert.assertEquals(2, methods.length); + String[] names = new String[] {methods[0].getName(), methods[1].getName()}; + Arrays.sort(names); + Assert.assertEquals(DecoratedForkJoinTask.Accessor.GET_DISCO_DECORATION_METHOD_NAME, names[0]); + Assert.assertEquals(DecoratedForkJoinTask.Accessor.SET_DISCO_DECORATION_METHOD_NAME, names[1]); } } From d6bee7e9b20b4aa9526329bc9772ace9a2b434f0 Mon Sep 17 00:00:00 2001 From: Connell Date: Wed, 8 Apr 2020 19:05:27 -0700 Subject: [PATCH 03/45] Allow plugins to declare a package of Installables as well as just individual Installables. This helps plugins maintain a list of their Installables in just a single place, instead of a duplication between code and manifest --- .../build.gradle.kts | 3 +- .../disco/agent/plugin/PluginDiscovery.java | 34 ++++++++----- .../agent/plugin/PluginDiscoveryTests.java | 49 +++++++++++++++---- .../agent/plugin/source/PluginPackage.java | 25 ++++++++++ disco-java-agent/disco-java-agent/README.md | 4 +- 5 files changed, 91 insertions(+), 24 deletions(-) create mode 100644 disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/source/PluginPackage.java diff --git a/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts b/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts index 34dea2d..7e6aa1c 100644 --- a/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts +++ b/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts @@ -40,8 +40,7 @@ dependencies { tasks.shadowJar { manifest { attributes(mapOf( - //this has to align with the contents of WebSupport.java. Would be good to find a way to avoid this duplication - "Disco-Installable-Classes" to "software.amazon.disco.agent.web.servlet.HttpServletServiceInterceptor software.amazon.disco.agent.web.apache.httpclient.ApacheHttpClientInterceptor" + "Disco-Installable-Classes" to "software.amazon.disco.agent.web.WebSupport" )) } } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java index 97c9da6..afca720 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java @@ -20,6 +20,7 @@ import software.amazon.disco.agent.event.Listener; import software.amazon.disco.agent.inject.Injector; import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.agent.interception.Package; import software.amazon.disco.agent.logging.LogManager; import software.amazon.disco.agent.logging.Logger; @@ -47,7 +48,7 @@ * * Each plugin is delivered as a JAR file, containing a Manifest with the following properties: * - * Disco-Installable-Classes: a space-separated list of fully qualified class names which are expected to inherit from Installable + * Disco-Installable-Classes: a space-separated list of fully qualified class names which are expected to inherit from either Installable or Package * and have a no-args constructor. Installables will be processed first, across all scanned plugins * Disco-Init-Class: if any further one-off-initialization is required, a fully qualified class may be provided. If this class provides a method * matching the signature "public static void init(void)", that method will be executed. All plugins will have this init() @@ -135,12 +136,23 @@ public static Set processInstallables() { Set installables = new HashSet<>(); if (installableClasses != null && !installableClasses.isEmpty()) { for (ClassInfo info : installableClasses) { - try { - Installable installable = (Installable) info.clazz.newInstance(); - installables.add(installable); - pluginOutcomes.get(info.pluginName).installables.add(installable); - } catch (Exception e) { - log.warn("DiSCo(Core) could not instantiate Installable " + info.clazz.getName(), e); + if (Installable.class.isAssignableFrom(info.clazz)) { + try { + Installable installable = (Installable)info.clazz.getDeclaredConstructor().newInstance(); + installables.add(installable); + pluginOutcomes.get(info.pluginName).installables.add(installable); + } catch (Exception e) { + log.warn("DiSCo(Core) could not instantiate Installable " + info.clazz.getName(), e); + } + } else if (Package.class.isAssignableFrom(info.clazz)) { + try { + Package pkg = (Package)info.clazz.getDeclaredConstructor().newInstance(); + Collection pkgInstallables = pkg.get(); + installables.addAll(pkgInstallables); + pluginOutcomes.get(info.pluginName).installables.addAll(pkgInstallables); + } catch (Exception e) { + log.warn("DiSCo(Core) could not instantiate Package " + info.clazz.getName(), e); + } } } } @@ -168,7 +180,7 @@ public static Collection apply() { if (listenerClasses != null && !listenerClasses.isEmpty()) { for (ClassInfo info : listenerClasses) { try { - Listener listener = (Listener) info.clazz.newInstance(); + Listener listener = (Listener) info.clazz.getDeclaredConstructor().newInstance(); EventBus.addListener(listener); pluginOutcomes.get(info.pluginName).listeners.add(listener); } catch (Exception e) { @@ -270,7 +282,7 @@ static void processInitClass(String pluginName, String initClassName, boolean bo /** * Helper method to discover the Classes specified for Installables in the plugin * @param pluginName the name of the plugin JAR file where the classes are defined - * @param installableClassNames the names of the Installable classes determined from the Manifest + * @param installableClassNames the names of the Installable or Package classes determined from the Manifest * @param bootstrap true if the plugin is requesting to be loaded by the bootstrap classloader * @throws Exception reflection errors may occur if the class cannot be found */ @@ -280,11 +292,11 @@ static void processInstallableClasses(String pluginName, String installableClass for (String className: classNames) { try { Class clazz = classForName(className.trim(), bootstrap); - if (Installable.class.isAssignableFrom(clazz)) { + if (Installable.class.isAssignableFrom(clazz) || Package.class.isAssignableFrom(clazz)) { ClassInfo installableInfo = new ClassInfo(pluginName, clazz, bootstrap); installableClasses.add(installableInfo); } else { - log.warn("DiSCo(Core) specified Installable is not an instance of Installable: " + className); + log.warn("DiSCo(Core) specified Installable is not an instance of Installable or Package: " + className); } } catch (ClassNotFoundException e) { log.warn("DiSCo(Core) cannot locate Installable: " + className); diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java index 5c742a7..5cd2c05 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java @@ -21,6 +21,7 @@ import software.amazon.disco.agent.event.EventBus; import software.amazon.disco.agent.interception.Installable; import software.amazon.disco.agent.plugin.source.PluginInit; +import software.amazon.disco.agent.plugin.source.PluginInstallable; import software.amazon.disco.agent.plugin.source.PluginListener; import org.junit.After; import org.junit.Assert; @@ -29,9 +30,11 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; +import software.amazon.disco.agent.plugin.source.PluginPackage; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; import java.lang.instrument.Instrumentation; import java.nio.file.Files; import java.nio.file.Paths; @@ -96,7 +99,7 @@ public void testPluginListenerNonBootstrap() throws Exception { @Test public void testPluginInstallableNonBootstrap() throws Exception { - createJar("plugin_with_installable", + createJar("plugin_with_package", "Disco-Installable-Classes: software.amazon.disco.agent.plugin.source.PluginInstallable", "software.amazon.disco.agent.plugin.source.PluginInstallable"); Collection outcomes = scanAndApply(instrumentation, agentConfig); @@ -107,6 +110,28 @@ public void testPluginInstallableNonBootstrap() throws Exception { Assert.assertFalse(outcome.bootstrap); } + @Test + public void testPluginPackageInstallableNonBootstrap() throws Exception { + createJar("plugin_with_installable", + "Disco-Installable-Classes: software.amazon.disco.agent.plugin.source.PluginPackage", + "software.amazon.disco.agent.plugin.source.PluginPackage", + "software.amazon.disco.agent.plugin.source.PluginInstallable", + "software.amazon.disco.agent.plugin.source.PluginPackage$OtherInstallable"); + Collection outcomes = scanAndApply(instrumentation, agentConfig); + PluginOutcome outcome = outcomes.iterator().next(); + Installable installable1 = outcome.installables.get(0); + Installable installable2 = outcome.installables.get(1); + Mockito.verify(instrumentation).appendToSystemClassLoaderSearch(Mockito.any()); + Assert.assertTrue(installables.contains(installable1)); + Assert.assertTrue(installables.contains(installable2)); + Assert.assertFalse(outcome.bootstrap); + Set classes = new HashSet<>(); + classes.add(installable1.getClass()); + classes.add(installable2.getClass()); + Assert.assertTrue(classes.contains(PluginInstallable.class)); + Assert.assertTrue(classes.contains(PluginPackage.OtherInstallable.class)); + } + @Test public void testPluginBootstrapFlag() throws Exception { createJar("plugin_with_bootstrap_true", @@ -123,7 +148,7 @@ private Collection scanAndApply(Instrumentation instrumentation, return PluginDiscovery.apply(); } - private void createJar(String name, String manifestContent, String className) throws Exception { + private void createJar(String name, String manifestContent, String... classNames) throws Exception { File file = tempFolder.newFile(name + ".jar"); try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { @@ -135,13 +160,19 @@ private void createJar(String name, String manifestContent, String className) th jarOutputStream.closeEntry(); //write class file - if (className != null) { - String classFull = className.replace('.', '/'); - String classPackage = classFull.substring(0, classFull.indexOf('/')) + "/"; - jarOutputStream.putNextEntry(new ZipEntry(classPackage)); - jarOutputStream.putNextEntry(new ZipEntry(classFull)); - jarOutputStream.write(getBytes(classFull)); - jarOutputStream.closeEntry(); + if (classNames != null) { + for (String className: classNames) { + String classFull = className.replace('.', '/'); + String classPackage = classFull.substring(0, classFull.indexOf('/')) + "/"; + try { + jarOutputStream.putNextEntry(new ZipEntry(classPackage)); + } catch (IOException e) { + //swallow, if this occurred due to creating an already-present folder + } + jarOutputStream.putNextEntry(new ZipEntry(classFull)); + jarOutputStream.write(getBytes(classFull)); + jarOutputStream.closeEntry(); + } } } } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/source/PluginPackage.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/source/PluginPackage.java new file mode 100644 index 0000000..725650e --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/source/PluginPackage.java @@ -0,0 +1,25 @@ +package software.amazon.disco.agent.plugin.source; + +import net.bytebuddy.agent.builder.AgentBuilder; +import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.agent.interception.Package; + +import java.util.Arrays; +import java.util.Collection; + +public class PluginPackage implements Package { + @Override + public Collection get() { + return Arrays.asList( + new PluginInstallable(), + new OtherInstallable() + ); + } + + public static class OtherInstallable implements Installable { + @Override + public AgentBuilder install(AgentBuilder agentBuilder) { + return null; + } + } +} diff --git a/disco-java-agent/disco-java-agent/README.md b/disco-java-agent/disco-java-agent/README.md index e9d566e..e68c1f5 100644 --- a/disco-java-agent/disco-java-agent/README.md +++ b/disco-java-agent/disco-java-agent/README.md @@ -18,7 +18,7 @@ Each Plugin advertises itself via the MANIFEST file inside the JAR. As an exampl
```manifest -Disco-Installable-Classes: com.my.org.SomeInterceptor com.my.org.SomeOtherInterceptor +Disco-Installable-Classes: com.my.org.SomeInterceptor com.my.org.SomeOtherInterceptor com.my.org.SomePackageOfInterceptors Disco-Listener-Classes: com.my.org.SomeListener com.my.org.SomeOtherListener Disco-Init-Class: com.my.org.SomeClassWithAnInitMethod Disco-Bootstrap-Classloader: true @@ -29,7 +29,7 @@ Disco-Bootstrap-Classloader: true | Entry | Meaning | | --- | --- | -| `Disco-Installable-Classes` | A space-separated list of fully qualified class names which are expected to inherit from Installable and have a no-args constructor. Installables will be processed first, across all scanned plugins | +| `Disco-Installable-Classes` | A space-separated list of fully qualified class names which are expected to inherit from either software.amazon.disco.agent.interception.Installable or software.amazon.disco.agent.interception.Package, and have a no-args constructor. Installables will be processed first, across all scanned plugins | | `Disco-Init-Class` | If any further one-off-initialization is required, a fully qualified class may be provided. If this class provides a method matching the signature "public static void init(void)", that method will be executed. All plugins will have this init() method processed *after* all plugins have had their Installables processed. | | `Disco-Listener-Classes` | A space-separated list of fully qualified class names which are expected to inherit from Listener and have a no-args constructor. Listener registration for all plugins will occure after one-off initialization for all plugins | `Disco-Bootstrap-Classloader` | If set to the literal case-insensitive string 'true', this JAR file will be added to the runtime's bootstrap classloader. Any other value, or the absence of this attribute, means the plugin will be loaded via the system classloader like a normal runtime dependency. It is not usually necessary to specify this attribute, unless Installables wish to intercept JDK classes. | From 479e33e8b4b828c046eb25e3340e34efe9141a49 Mon Sep 17 00:00:00 2001 From: Connell Date: Tue, 14 Apr 2020 15:42:15 -0700 Subject: [PATCH 04/45] Add a new abstraction for declaratively accessing methods inside Objects during interception, when the type of the object is not a compile time dependency, and without reflection. Migrate Web support to the new idiom. --- .../ApacheHttpClientInterceptorTests.java | 61 ++++-- .../HttpServletServiceInterceptorTests.java | 5 + .../disco/agent/web/HeaderAccessor.java | 7 - .../amazon/disco/agent/web/WebSupport.java | 17 +- .../ApacheHttpClientInterceptor.java | 45 +++- .../web/apache/utils/HttpRequestAccessor.java | 130 ++--------- .../apache/utils/HttpRequestBaseAccessor.java | 37 ++++ .../apache/utils/HttpResponseAccessor.java | 72 +------ .../web/servlet/HttpServletInterceptor.java | 67 ------ .../servlet/HttpServletRequestAccessor.java | 134 ++++-------- .../servlet/HttpServletResponseAccessor.java | 59 ++--- .../HttpServletServiceInterceptor.java | 52 ++++- .../disco/agent/web/WebSupportTests.java | 19 ++ .../ApacheHttpClientInterceptorTests.java | 76 +++++-- .../utils/HttpRequestAccessorTests.java | 114 ---------- .../utils/HttpResponseAccessorTests.java | 53 ----- .../servlet/HttpServletInterceptorTests.java | 2 +- .../HttpServletRequestAccessorTests.java | 25 ++- .../HttpServletResponseAccessorTests.java | 13 +- .../HttpServletServiceInterceptorTests.java | 8 +- .../agent/reflect/MethodHandleWrapper.java | 4 + .../interception/InterceptionInstaller.java | 9 - .../agent/plugin/PluginDiscoveryTests.java | 3 +- .../build.gradle.kts | 2 + .../annotations/DataAccessPath.java | 40 ++++ .../interception/templates/DataAccessor.java | 204 ++++++++++++++++++ .../templates/DataAccessorTests.java | 134 ++++++++++++ .../templates/integtest/ExampleAccessor.java | 28 +++ .../integtest/ExampleAccessorInstaller.java | 35 +++ .../source/ExampleDelegatedClass.java | 23 ++ .../integtest/source/ExampleOuterClass.java | 32 +++ 31 files changed, 883 insertions(+), 627 deletions(-) create mode 100644 disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpRequestBaseAccessor.java delete mode 100644 disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletInterceptor.java create mode 100644 disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/WebSupportTests.java delete mode 100644 disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/HttpRequestAccessorTests.java delete mode 100644 disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/HttpResponseAccessorTests.java create mode 100644 disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/annotations/DataAccessPath.java create mode 100644 disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/templates/DataAccessor.java create mode 100644 disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/DataAccessorTests.java create mode 100644 disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessor.java create mode 100644 disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessorInstaller.java create mode 100644 disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleDelegatedClass.java create mode 100644 disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleOuterClass.java diff --git a/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/apache/httpclient/ApacheHttpClientInterceptorTests.java b/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/apache/httpclient/ApacheHttpClientInterceptorTests.java index 7155492..aa99d57 100644 --- a/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/apache/httpclient/ApacheHttpClientInterceptorTests.java +++ b/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/apache/httpclient/ApacheHttpClientInterceptorTests.java @@ -15,12 +15,17 @@ package software.amazon.disco.agent.integtest.web.apache.httpclient; +import org.apache.http.HttpHost; +import org.apache.http.ProtocolVersion; import org.apache.http.RequestLine; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHttpRequest; +import org.apache.http.message.BasicRequestLine; +import software.amazon.disco.agent.event.HttpServiceDownstreamRequestEvent; import software.amazon.disco.agent.event.HttpServiceDownstreamResponseEvent; import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent; import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; @@ -32,21 +37,19 @@ import software.amazon.disco.agent.event.Listener; import software.amazon.disco.agent.event.ServiceRequestEvent; import software.amazon.disco.agent.event.ServiceResponseEvent; -import org.apache.http.HttpRequest; import org.junit.Assert; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.IOException; +import java.net.URI; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class ApacheHttpClientInterceptorTests { private TestListener testListener; @@ -96,10 +99,47 @@ public void testDefaultClient() throws Exception { assertEquals(1, testListener.responseEvents.size()); } + @Test + public void testDefaultClientWithBasicHttpRequest() throws Exception { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + BasicHttpRequest request = new BasicHttpRequest(new BasicRequestLine("GET", "https://amazon.com", new ProtocolVersion("protocol", 1, 1))); + try { + httpClient.execute(new HttpHost("host", 80), request); + } catch (IOException e) { + //swallow + } + } + + assertEquals(1, testListener.requestEvents.size()); + assertEquals(1, testListener.responseEvents.size()); + } + + @Test + public void testGetRequestLineNotCalledForGet() throws Exception { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet request = new HttpGet("https://amazon.com") { + @Override + public RequestLine getRequestLine() { + //return the incorrect details to prove that this request line was not the source of the event data + return new BasicRequestLine("WRONG", "http://wrong.com", new ProtocolVersion("protocol", 1, 1)); + } + }; + try { + httpClient.execute(request); + } catch (IOException e) { + //swallow + } + } + + HttpServiceDownstreamRequestEvent event = (HttpServiceDownstreamRequestEvent)testListener.requestEvents.get(0); + Assert.assertEquals("GET", event.getMethod()); + Assert.assertEquals("https://amazon.com", event.getUri()); + + } + @Test public void testExecuteInterceptionChained() throws Exception { - HttpUriRequest request = mock(HttpUriRequest.class); - setUpRequest(request); + HttpUriRequest request = new HttpGet(new URI(SOME_URI)); // Set up victim http client FakeChainedExecuteCallHttpClientReturnResponse httpClient = new FakeChainedExecuteCallHttpClientReturnResponse(); @@ -127,8 +167,7 @@ public void testExecuteInterceptionChained() throws Exception { @Test(expected = IOException.class) public void testExecuteInterceptionException() throws Exception { - HttpUriRequest request = mock(HttpUriRequest.class); - setUpRequest(request); + HttpUriRequest request = new HttpGet(new URI(SOME_URI)); // Set up victim http client FakeChainedExecuteCallHttpClientThrowException httpClient = new FakeChainedExecuteCallHttpClientThrowException(); @@ -175,14 +214,6 @@ public void listen(Event e) { } } - private static void setUpRequest(final HttpRequest request) { - RequestLine requestLine = mock(RequestLine.class); - - when(request.getRequestLine()).thenReturn(requestLine); - when(requestLine.getUri()).thenReturn(SOME_URI); - when(requestLine.getMethod()).thenReturn(METHOD); - } - private static void verifyServiceRequestEvent(final ServiceRequestEvent serviceDownstreamRequestEvent) { assertTrue(serviceDownstreamRequestEvent instanceof ServiceDownstreamRequestEvent); assertEquals(METHOD, serviceDownstreamRequestEvent.getOperation()); diff --git a/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/servlet/HttpServletServiceInterceptorTests.java b/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/servlet/HttpServletServiceInterceptorTests.java index 0fec21a..21b6791 100644 --- a/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/servlet/HttpServletServiceInterceptorTests.java +++ b/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/servlet/HttpServletServiceInterceptorTests.java @@ -36,7 +36,9 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -199,6 +201,9 @@ private void testServlet(HttpServlet servlet) throws Throwable { Mockito.when(response.getHeader("more-custom-header")).thenReturn("some-custom-data"); Mockito.when(response.getStatus()).thenReturn(200); + //wrap objects, otherwise Mockito will mock out the methods in the Accessor interface, such as retrieveHeaderMap + request = new HttpServletRequestWrapper(request); + response = new HttpServletResponseWrapper(response); servlet.service(request, response); // Ensure that we get only two events, even when a subclass and a superclass are both instrumented. diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/HeaderAccessor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/HeaderAccessor.java index 38e5cf2..434859f 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/HeaderAccessor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/HeaderAccessor.java @@ -28,11 +28,4 @@ public interface HeaderAccessor { * @return a String-to-String map of headers */ Map retrieveHeaderMap(); - - /** - * Get a named header from the servlet request or response - * @param name the name of the header - * @return the value of the header, or null if absent - */ - String getHeader(String name); } diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/WebSupport.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/WebSupport.java index adeb44f..e60a08b 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/WebSupport.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/WebSupport.java @@ -17,7 +17,13 @@ import software.amazon.disco.agent.interception.Installable; import software.amazon.disco.agent.interception.Package; +import software.amazon.disco.agent.interception.templates.DataAccessor; import software.amazon.disco.agent.web.apache.httpclient.ApacheHttpClientInterceptor; +import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; +import software.amazon.disco.agent.web.apache.utils.HttpRequestBaseAccessor; +import software.amazon.disco.agent.web.apache.utils.HttpResponseAccessor; +import software.amazon.disco.agent.web.servlet.HttpServletRequestAccessor; +import software.amazon.disco.agent.web.servlet.HttpServletResponseAccessor; import software.amazon.disco.agent.web.servlet.HttpServletServiceInterceptor; import java.util.Arrays; @@ -34,7 +40,16 @@ public class WebSupport implements Package { public Collection get() { return Arrays.asList( new HttpServletServiceInterceptor(), - new ApacheHttpClientInterceptor() + new ApacheHttpClientInterceptor(), + + //accessors + DataAccessor.forConcreteSubclassesOfInterface("org.apache.http.HttpRequest", HttpRequestAccessor.class), + DataAccessor.forClassNamed("org.apache.http.client.methods.HttpRequestBase", HttpRequestBaseAccessor.class), + DataAccessor.forConcreteSubclassesOfInterface("org.apache.http.HttpResponse", HttpResponseAccessor.class), + + DataAccessor.forConcreteSubclassesOfInterface("javax.servlet.http.HttpServletResponse", HttpServletResponseAccessor.class), + DataAccessor.forConcreteSubclassesOfInterface("javax.servlet.http.HttpServletRequest", HttpServletRequestAccessor.class) + ); } } diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java index 8cddb8b..bd1193d 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java @@ -19,6 +19,7 @@ import software.amazon.disco.agent.event.HttpServiceDownstreamResponseEvent; import software.amazon.disco.agent.web.apache.event.ApacheEventFactory; import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; +import software.amazon.disco.agent.web.apache.utils.HttpRequestBaseAccessor; import software.amazon.disco.agent.web.apache.utils.HttpResponseAccessor; import software.amazon.disco.agent.web.apache.utils.MethodInterceptionCounter; import software.amazon.disco.agent.event.EventBus; @@ -79,7 +80,7 @@ public static Object intercept(@AllArguments final Object[] args, return call(zuper); } - HttpRequestAccessor requestAccessor = new HttpRequestAccessor(args); + HttpRequestAccessor requestAccessor = findRequestObject(args); // publish request event ServiceDownstreamRequestEvent requestEvent = publishRequestEvent(requestAccessor); @@ -92,7 +93,7 @@ public static Object intercept(@AllArguments final Object[] args, throwable = t; } finally { // publish response event - HttpResponseAccessor responseAccessor = new HttpResponseAccessor(response); + HttpResponseAccessor responseAccessor = (HttpResponseAccessor)response; publishResponseEvent(responseAccessor, requestEvent, throwable); if (throwable != null) { throw throwable; @@ -101,6 +102,22 @@ public static Object intercept(@AllArguments final Object[] args, } } + /** + * Find the first (presumably only) HttpRequest object in the args passed to the intercepted method. It is assumed that the DataAccessor interceptor is installed, and that therefore + * this object will be castable to the HttpRequestAccessor type. + * @param args all arguments passed to the intercepted method + * @return the found HttpRequest, converted to its HttpRequestAccessor form, or null if none found + */ + private static HttpRequestAccessor findRequestObject(Object[] args) { + for (int i = 0; i < args.length; i++) { + if (HttpRequestAccessor.class.isAssignableFrom(args[i].getClass())) { + return (HttpRequestAccessor)args[i]; + } + } + + return null; + } + /** * Do the actual downstream call. * @@ -125,9 +142,21 @@ private static Object call(final Callable zuper) throws Throwable { * @return The published ServiceDownstreamRequestEvent, which is needed when publishing ServiceDownstreamResponseEvent later */ private static HttpServiceDownstreamRequestEvent publishRequestEvent(final HttpRequestAccessor requestAccessor) { - HttpServiceDownstreamRequestEvent requestEvent = ApacheEventFactory.createDownstreamRequestEvent(APACHE_HTTP_CLIENT_ORIGIN, requestAccessor.getUri(), requestAccessor.getMethod(), requestAccessor); - requestEvent.withMethod(requestAccessor.getMethod()); - requestEvent.withUri(requestAccessor.getUri()); + String uri; + String method; + if (requestAccessor instanceof HttpRequestBaseAccessor) { + //we can retrieve the data in a streamlined way, avoiding internal production of the RequestLine + HttpRequestBaseAccessor baseAccessor = (HttpRequestBaseAccessor)requestAccessor; + uri = baseAccessor.getUri(); + method = baseAccessor.getMethod(); + } else { + uri = requestAccessor.getUriFromRequestLine(); + method = requestAccessor.getMethodFromRequestLine(); + } + //TODO - using uri and method as service and operation name is unsatisfactory. + HttpServiceDownstreamRequestEvent requestEvent = ApacheEventFactory.createDownstreamRequestEvent(APACHE_HTTP_CLIENT_ORIGIN, uri, method, requestAccessor); + requestEvent.withMethod(method); + requestEvent.withUri(uri); EventBus.publish(requestEvent); return requestEvent; } @@ -143,8 +172,10 @@ private static void publishResponseEvent(final HttpResponseAccessor responseAcce if(throwable != null) { responseEvent.withThrown(throwable); } - responseEvent.withStatusCode(responseAccessor.getStatusCode()); - responseEvent.withContentLength(responseAccessor.getContentLength()); + if (responseAccessor != null) { + responseEvent.withStatusCode(responseAccessor.getStatusCode()); + responseEvent.withContentLength(responseAccessor.getContentLength()); + } EventBus.publish(responseEvent); } diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpRequestAccessor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpRequestAccessor.java index 639b91b..bf70b2a 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpRequestAccessor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpRequestAccessor.java @@ -15,130 +15,38 @@ package software.amazon.disco.agent.web.apache.utils; -import software.amazon.disco.agent.reflect.MethodHandleWrapper; +import software.amazon.disco.agent.interception.annotations.DataAccessPath; /** - * Concrete accessor for the methods reflectively accessed within HttpRequest. + * Data Accessor for any subtype of HttpRequest */ -public class HttpRequestAccessor { - private static final String HTTP_REQUEST_CLASS_NAME = "org.apache.http.HttpRequest"; - - private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); - - //these methods only use simple types, so can be initialized inline without any exception handling - private static MethodHandleWrapper addHeader = new MethodHandleWrapper(HTTP_REQUEST_CLASS_NAME, classLoader, "addHeader", void.class, String.class, String.class); - private static MethodHandleWrapper removeHeaders = new MethodHandleWrapper(HTTP_REQUEST_CLASS_NAME, classLoader, "removeHeaders", void.class, String.class); - - private static MethodHandleWrapper getRequestLine; - private static MethodHandleWrapper getMethod; - private static MethodHandleWrapper getUri; - - private final Object requestObject; - - { - try { - //TODO unsafe to leave these as null if they fail? - getRequestLine = new MethodHandleWrapper(HTTP_REQUEST_CLASS_NAME, classLoader, "getRequestLine", "org.apache.http.RequestLine"); - getMethod = new MethodHandleWrapper(getRequestLine.getRtype().getName(), classLoader, "getMethod", String.class); - getUri = new MethodHandleWrapper(getRequestLine.getRtype().getName(), classLoader, "getUri", String.class); - } catch (Throwable t) { - //do nothing? - } - } - - /** - * Construct a new HttpRequestAccessor with a concrete HttpRequest object. - * - * @param args The args of HttpClient.execute, in which contains a concrete HttpRequest object to inspect - */ - public HttpRequestAccessor(final Object...args) { - this.requestObject = findRequestObject(args); - } - - /** - * Removes all headers with a certain name from this request. - * - * @param name The name of the headers to remove - */ - public void removeHeaders(final String name) { - if (requestObject == null) { - return; - } - removeHeaders.invoke(requestObject, name); - } - +public interface HttpRequestAccessor { /** - * Adds a header to this message. The header will be appended to the end of the list. - * - * @param name The name of the header - * @param value The value of the header + * Get the HTTP method from the request's request line. + * The method is explicitly named, because of the range of concrete classes implementing HttpRequest. Some, like HttpGet, + * implement a method named 'getMethod' explicitly. Others, like BasicHttpRequest, do not. */ - public void addHeader(final String name, final String value) { - addHeader.invoke(requestObject, name, value); - } + @DataAccessPath("getRequestLine()/getMethod()") + String getMethodFromRequestLine(); /** - * Helper method to safely try to call the getMethod method of RequestLine within this request. - * - * @return The http method of this request + * Get the URI from the request's request line + * @return the URI */ - public String getMethod() { - if (requestObject == null) { - return null; - } - Object requestLine = getRequestLine.invoke(requestObject); - if (requestLine != null) { - return (String) getMethod.invoke(requestLine); - } - return null; - } + @DataAccessPath("getRequestLine()/getUri()") + String getUriFromRequestLine(); - /** - * Helper method to safely try to call the getUri method of RequestLine within this request. - * - * @return The http uri of this request - */ - public String getUri() { - if (requestObject == null) { - return null; - } - Object requestLine = getRequestLine.invoke(requestObject); - if (requestLine != null) { - return (String) getUri.invoke(requestLine); - } - return null; - } /** - * Find the FIRST concrete HttpRequest object of the given type from a list of objects. - * - * @param args The args of HttpClient.execute, in which contains a concrete object to inspect - * @return The first concrete object of the given type, or null if cannot find any + * Add a new HTTP header to the request + * @param name the header name + * @param value the header value */ - static Object findRequestObject(final Object... args) { - Class httpRequestClass = maybeFindClass(HTTP_REQUEST_CLASS_NAME); - - // prefer array access via indexes over foreach for the sake of performance - for (int i = 0; i < args.length; i++) { - if (httpRequestClass != null && httpRequestClass.isInstance(args[i])) { - return args[i]; - } - } - return null; - } + void addHeader(String name, String value); /** - * Lookup a Class by name if encountering for the first time. - * @param accessClassName The class name which this accessor accesses - * @return The Class which this accessor accesses + * Remove all headers with the given name from the request + * @param name the header name */ - private static Class maybeFindClass(final String accessClassName) { - try { - return Class.forName(accessClassName, true, ClassLoader.getSystemClassLoader()); - } catch (ClassNotFoundException e) { - // do nothing - } - - return null; - } + void removeHeaders(String name); } diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpRequestBaseAccessor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpRequestBaseAccessor.java new file mode 100644 index 0000000..62fb3a9 --- /dev/null +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpRequestBaseAccessor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.web.apache.utils; + +import software.amazon.disco.agent.interception.annotations.DataAccessPath; + +/** + * A streamlined Data Accessor for requests such as HttpGet, HttpDelete and family, to avoid the expensive creation of + * a RequestLine, when getting method and URI + */ +public interface HttpRequestBaseAccessor { + /** + * Get the HTTP method from the HttpRequestBase concrete class + * @return the HTTP method e.g. "GET". + */ + String getMethod(); + + /** + * Get the URI from the HttpRequestBase concrete class. + * @return the URI from this request + */ + @DataAccessPath("getURI()/toString()") + String getUri(); +} diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpResponseAccessor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpResponseAccessor.java index 041dbf9..0799c19 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpResponseAccessor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/HttpResponseAccessor.java @@ -15,73 +15,23 @@ package software.amazon.disco.agent.web.apache.utils; -import software.amazon.disco.agent.reflect.MethodHandleWrapper; +import software.amazon.disco.agent.interception.annotations.DataAccessPath; /** - * Accessor for data stored in Apache's HttpResponse + * Data Accessor for subtypes of HttpResponse. */ -public class HttpResponseAccessor { - private static final String HTTP_RESPONSE_CLASS_NAME = "org.apache.http.HttpResponse"; - - private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); - - private static MethodHandleWrapper getStatusLine; - private static MethodHandleWrapper getStatusCode; - - private static MethodHandleWrapper getEntity; - private static MethodHandleWrapper getContentLength; - - private final Object responseObject; - - { - try { - getStatusLine = new MethodHandleWrapper(HTTP_RESPONSE_CLASS_NAME, classLoader, "getStatusLine", "org.apache.http.StatusLine"); - getStatusCode = new MethodHandleWrapper(getStatusLine.getRtype().getName(), classLoader, "getStatusCode", int.class); - - getEntity = new MethodHandleWrapper(HTTP_RESPONSE_CLASS_NAME, classLoader, "getEntity", "org.apache.http.HttpEntity"); - getContentLength = new MethodHandleWrapper(getEntity.getRtype().getName(), classLoader, "getContentLength", long.class); - } catch (Exception e) { - //do nothing - } - } - - - /** - * Create a new accessor of an HttpResponse object - * @param response the HttpResponseObject - */ - public HttpResponseAccessor(Object response) { - this.responseObject = response; - } - +public interface HttpResponseAccessor { /** - * Get the status code stored in the response - * @return status code + * Get the status code from the status line in the response + * @return the http status code */ - public int getStatusCode() { - if (responseObject == null || getStatusLine == null) { - return -1; - } - Object statusLine = getStatusLine.invoke(responseObject); - if (statusLine != null) { - return (int) getStatusCode.invoke(statusLine); - } - return -1; - } + @DataAccessPath("getStatusLine()/getStatusCode()") + int getStatusCode(); /** - * Get the content length stored in the response - * @return content length + * Get the content length of the entity in the response + * @return the entity content length */ - public long getContentLength() { - if (responseObject == null || getContentLength == null) { - return -1L; - } - - Object entity = getEntity.invoke(responseObject); - if (entity == null) { - return 0L; - } - return (long)getContentLength.invoke(entity); - } + @DataAccessPath("getEntity()/getContentLength()") + long getContentLength(); } diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletInterceptor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletInterceptor.java deleted file mode 100644 index 113f5aa..0000000 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletInterceptor.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.agent.web.servlet; - -import software.amazon.disco.agent.interception.Installable; -import net.bytebuddy.agent.builder.AgentBuilder; -import net.bytebuddy.description.method.MethodDescription; -import net.bytebuddy.description.type.TypeDescription; -import net.bytebuddy.implementation.MethodDelegation; -import net.bytebuddy.matcher.ElementMatcher; -import net.bytebuddy.matcher.ElementMatchers; - -/** - * Base class for the HTTP Servlet Interceptor for inbound requests and outbound responses. - */ -public abstract class HttpServletInterceptor implements Installable { - - /** - * Build a ElementMatcher which defines the kind of class which will be intercepted. Package-private for tests. - * - * @return A ElementMatcher suitable to pass to the type() method of an AgentBuilder - */ - ElementMatcher buildClassMatcher() { - return ElementMatchers.hasSuperType(ElementMatchers.named("javax.servlet.http.HttpServlet")); - } - - /** - * Build an ElementMatcher which will match against the service() method of an HttpServlet. - * Package-private for tests - * - * @return An ElementMatcher suitable for passing to the method() method of a DynamicType.Builder - */ - ElementMatcher buildMethodMatcher() { - ElementMatcher requestTypeName = ElementMatchers.named("javax.servlet.http.HttpServletRequest"); - ElementMatcher responseTypeName = ElementMatchers.named("javax.servlet.http.HttpServletResponse"); - ElementMatcher.Junction hasTwoArgs = ElementMatchers.takesArguments(2); - ElementMatcher.Junction firstArgMatches = ElementMatchers.takesArgument(0, requestTypeName); - ElementMatcher.Junction secondArgMatches = ElementMatchers.takesArgument(1, responseTypeName); - ElementMatcher.Junction methodMatches = ElementMatchers.named("service").and(hasTwoArgs.and(firstArgMatches.and(secondArgMatches))); - return methodMatches.and(ElementMatchers.not(ElementMatchers.isAbstract())); - } - - /** - * {@inheritDoc} - */ - @Override - public AgentBuilder install(AgentBuilder agentBuilder) { - return agentBuilder - .type(buildClassMatcher()) - .transform((builder, typeDescription, classLoader, module) -> builder - .method(buildMethodMatcher()) - .intercept(MethodDelegation.to(this.getClass()))); - } -} diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletRequestAccessor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletRequestAccessor.java index d6866ee..e10846d 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletRequestAccessor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletRequestAccessor.java @@ -15,7 +15,7 @@ package software.amazon.disco.agent.web.servlet; -import software.amazon.disco.agent.reflect.MethodHandleWrapper; +import software.amazon.disco.agent.interception.annotations.DataAccessPath; import software.amazon.disco.agent.web.HeaderAccessor; import java.util.Collections; @@ -24,121 +24,77 @@ import java.util.Map; /** - * Concrete accessor for the methods reflectively accessed within HttpServletRequest + * Data Accessor for HttpServletRequest subclasses */ -public class HttpServletRequestAccessor implements HeaderAccessor { - private static final String SERVLET_REQUEST_CLASS_NAME = "javax.servlet.http.HttpServletRequest"; - private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); - - private static final MethodHandleWrapper getHeaderNames = new MethodHandleWrapper(SERVLET_REQUEST_CLASS_NAME, classLoader, "getHeaderNames", Enumeration.class); - private static final MethodHandleWrapper getHeader = new MethodHandleWrapper(SERVLET_REQUEST_CLASS_NAME, classLoader, "getHeader", String.class, String.class); - private static final MethodHandleWrapper getRemotePort = new MethodHandleWrapper(SERVLET_REQUEST_CLASS_NAME, classLoader, "getRemotePort", int.class); - private static final MethodHandleWrapper getLocalPort = new MethodHandleWrapper(SERVLET_REQUEST_CLASS_NAME, classLoader, "getLocalPort", int.class); - private static final MethodHandleWrapper getRemoteAddr = new MethodHandleWrapper(SERVLET_REQUEST_CLASS_NAME, classLoader, "getRemoteAddr", String.class); - private static final MethodHandleWrapper getLocalAddr = new MethodHandleWrapper(SERVLET_REQUEST_CLASS_NAME, classLoader, "getLocalAddr", String.class); - private static final MethodHandleWrapper getMethod = new MethodHandleWrapper(SERVLET_REQUEST_CLASS_NAME, classLoader, "getMethod", String.class); - private static final MethodHandleWrapper getRequestURL = new MethodHandleWrapper(SERVLET_REQUEST_CLASS_NAME, classLoader, "getRequestURL", StringBuffer.class); - - private final Object requestObject; - +public interface HttpServletRequestAccessor extends HeaderAccessor { /** - * Construct a new HttpServletRequestAccessor with a concrete request object - * @param request the HttpServletRequest to inspect + * Get an enumeration of header names from the request + * @return the header names */ - HttpServletRequestAccessor(Object request) { - this.requestObject = request; - } + Enumeration getHeaderNames(); /** - * {@inheritDoc} + * Get the value of a named header + * @param name the name of the header + * @return the value of the named header */ - @Override - public String getHeader(String name) { - try { - return (String) getHeader.invoke(requestObject, name); - } catch (Throwable t) { - return null; - } - } + String getHeader(String name); /** - * {@inheritDoc} + * Get the remote port number from the request + * @return the remote port number */ - @Override - public Map retrieveHeaderMap() { - Map ret = new HashMap<>(); - try { - Enumeration headerNames = getHeaderNames(); - if (headerNames == null) { - return ret; - } - - for (String name : Collections.list(headerNames)) { - ret.put(name, getHeader(name)); - } - } catch (Throwable t) { - //do nothing - } - return ret; - } + int getRemotePort(); /** - * Get the IP port from the client-side request - * @return the IP port used by the remote client + * Get the local port number from the request + * @return the local port number */ - int getRemotePort() { - return (int)getRemotePort.invoke(requestObject); - } + int getLocalPort(); /** - * Get the IP address from the client-side request - * @return the IP address used by the remote client + * Get the remote address, IP address or DNS name, from the request + * @return the remote address */ - String getRemoteAddr() { - return (String)getRemoteAddr.invoke(requestObject); - } + String getRemoteAddr(); /** - * Get the IP port on the server-side - * @return the IP port used by the service to receive the request + * Get the local address, IP address or DNS name, from the request + * @return the local address */ - int getLocalPort() { - return (int)getLocalPort.invoke(requestObject); - } + String getLocalAddr(); /** - * Get the IP address on the server-side - * @return the IP address used by the service to receive the request + * Get the HTTP method name, e.g. "GET" from the request + * @return the method name */ - String getLocalAddr() { - return (String)getLocalAddr.invoke(requestObject); - } + String getMethod(); /** - * Return the HTTP method verb which was used for the request e.g. GET, PUT, ... - * @return the HTTP verb used in the request + * Get the URL from the request + * @return the URL */ - String getMethod() { - return (String)getMethod.invoke(requestObject); - } + @DataAccessPath("getRequestURL()/toString()") + String getRequestUrl(); /** - * Get the full URL used in the request - * @return the request URL + * {@inheritDoc} */ - String getRequestURL() { - StringBuffer requestUrl = (StringBuffer)getRequestURL.invoke(requestObject); - if (requestUrl == null) { - return null; - } - return requestUrl.toString(); - } + @Override + default Map retrieveHeaderMap() { + Map ret = new HashMap<>(); + try { + Enumeration headerNames = getHeaderNames(); + if (headerNames == null) { + return ret; + } - /** - * Helper method to retrieve an enumeration of header names from the response - * @return an enumeration of all named headers - */ - private Enumeration getHeaderNames() { - return (Enumeration)getHeaderNames.invoke(requestObject); + for (String name : Collections.list(headerNames)) { + ret.put(name, getHeader(name)); + } + } catch (Throwable t) { + //do nothing + } + return ret; } } diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletResponseAccessor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletResponseAccessor.java index 5abc066..23f14f3 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletResponseAccessor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletResponseAccessor.java @@ -15,52 +15,37 @@ package software.amazon.disco.agent.web.servlet; -import software.amazon.disco.agent.reflect.MethodHandleWrapper; import software.amazon.disco.agent.web.HeaderAccessor; import java.util.Collection; import java.util.HashMap; import java.util.Map; -/** - * Concrete accessor for the methods reflectively accessed within HttpServletResponse - */ -public class HttpServletResponseAccessor implements HeaderAccessor { - private static final String SERVLET_RESPONSE_CLASS_NAME = "javax.servlet.http.HttpServletResponse"; - - private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); - static final MethodHandleWrapper getHeaderNames = new MethodHandleWrapper(SERVLET_RESPONSE_CLASS_NAME, classLoader, "getHeaderNames", Collection.class); - static final MethodHandleWrapper getHeader = new MethodHandleWrapper(SERVLET_RESPONSE_CLASS_NAME, classLoader, "getHeader", String.class, String.class); - static final MethodHandleWrapper getStatus = new MethodHandleWrapper(SERVLET_RESPONSE_CLASS_NAME, classLoader, "getStatus", int.class); - - - private final Object responseObject; +public interface HttpServletResponseAccessor extends HeaderAccessor { + /** + * Get a collection of all header names from the servlet response + * @return collection of header names + */ + Collection getHeaderNames(); /** - * Construct a new HttpServletResponseAccessor with a concrete request object - * @param response the HttpServletResponse to inspect + * Get the value of the named header + * @param name the name of the header + * @return the value of the named header */ - HttpServletResponseAccessor(Object response) { - this.responseObject = response; - } + String getHeader(String name); /** - * {@inheritDoc} + * get the status code from the response + * @return the status code */ - @Override - public String getHeader(String name) { - try { - return (String) getHeader.invoke(responseObject, name); - } catch (Throwable t) { - return null; - } - } + int getStatus(); /** * {@inheritDoc} */ @Override - public Map retrieveHeaderMap() { + default Map retrieveHeaderMap() { Map ret = new HashMap<>(); try { Collection headerNames = getHeaderNames(); @@ -81,20 +66,4 @@ public Map retrieveHeaderMap() { return ret; } - - /** - * Get the HTTP status code from the response - * @return the status code e.g. 200 - */ - int getStatus() { - return (int)getStatus.invoke(responseObject); - } - - /** - * Helper method to retrieve a collection of header names from the response - * @return an iterable collection of all named headers - */ - private Collection getHeaderNames() { - return (Collection)getHeaderNames.invoke(responseObject); - } } diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptor.java index d26b49b..b7f5ced 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptor.java @@ -15,10 +15,17 @@ package software.amazon.disco.agent.web.servlet; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; import software.amazon.disco.agent.concurrent.TransactionContext; import software.amazon.disco.agent.event.EventBus; import software.amazon.disco.agent.event.HttpServletNetworkRequestEvent; import software.amazon.disco.agent.event.HttpServletNetworkResponseEvent; +import software.amazon.disco.agent.interception.Installable; import software.amazon.disco.agent.logging.LogManager; import software.amazon.disco.agent.logging.Logger; import net.bytebuddy.implementation.bind.annotation.AllArguments; @@ -32,7 +39,7 @@ * When the service() method of HttpServlet or subclass of it is called, * the method is intercepted to generate HttpNetworkProtocol(Request/Response)Events. */ -public class HttpServletServiceInterceptor extends HttpServletInterceptor { +public class HttpServletServiceInterceptor implements Installable { private final static Logger log = LogManager.getLogger(HttpServletServiceInterceptor.class); private static final String EVENT_ORIGIN = "httpServlet"; @@ -44,6 +51,18 @@ public class HttpServletServiceInterceptor extends HttpServletInterceptor { private static final String REFERER_HEADER = "referer"; private static final String USER_AGENT_HEADER = "user-agent"; + /** + * {@inheritDoc} + */ + @Override + public AgentBuilder install(AgentBuilder agentBuilder) { + return agentBuilder + .type(buildClassMatcher()) + .transform((builder, typeDescription, classLoader, module) -> builder + .method(buildMethodMatcher()) + .intercept(MethodDelegation.to(HttpServletServiceInterceptor.class))); + } + /** * The HttpServlet#service method is intercepted, and redirected here, where the * original request and response objects are sifted through to retrieve useful @@ -76,7 +95,7 @@ public static void service(@AllArguments Object[] args, try { // To reduce the # of dependencies, we use reflection to obtain the basic methods. Object request = args[0]; - HttpServletRequestAccessor reqAccessor = new HttpServletRequestAccessor(request); + HttpServletRequestAccessor reqAccessor = (HttpServletRequestAccessor)request; // Obtain the metadata information from the host. // If they are null, they are't stored, so retrieval would be null as well. @@ -93,7 +112,7 @@ public static void service(@AllArguments Object[] args, .withUserAgent(reqAccessor.getHeader(USER_AGENT_HEADER)) .withMethod(reqAccessor.getMethod()) .withRequest(request) - .withURL(reqAccessor.getRequestURL()); + .withURL(reqAccessor.getRequestUrl()); EventBus.publish(requestEvent); } catch (Throwable e) { log.error("DiSCo(Web) Failed to retrieve request data from servlet service."); @@ -110,7 +129,7 @@ public static void service(@AllArguments Object[] args, if (txStackDepth == 0) { try { Object response = args[1]; - HttpServletResponseAccessor respAccessor = new HttpServletResponseAccessor(response); + HttpServletResponseAccessor respAccessor = (HttpServletResponseAccessor)response; int statusCode = respAccessor.getStatus(); responseEvent = new HttpServletNetworkResponseEvent(EVENT_ORIGIN, requestEvent) @@ -131,4 +150,29 @@ public static void service(@AllArguments Object[] args, throw throwable; } } + + /** + * Build a ElementMatcher which defines the kind of class which will be intercepted. Package-private for tests. + * + * @return A ElementMatcher suitable to pass to the type() method of an AgentBuilder + */ + ElementMatcher buildClassMatcher() { + return ElementMatchers.hasSuperType(ElementMatchers.named("javax.servlet.http.HttpServlet")); + } + + /** + * Build an ElementMatcher which will match against the service() method of an HttpServlet. + * Package-private for tests + * + * @return An ElementMatcher suitable for passing to the method() method of a DynamicType.Builder + */ + ElementMatcher buildMethodMatcher() { + ElementMatcher requestTypeName = ElementMatchers.named("javax.servlet.http.HttpServletRequest"); + ElementMatcher responseTypeName = ElementMatchers.named("javax.servlet.http.HttpServletResponse"); + ElementMatcher.Junction hasTwoArgs = ElementMatchers.takesArguments(2); + ElementMatcher.Junction firstArgMatches = ElementMatchers.takesArgument(0, requestTypeName); + ElementMatcher.Junction secondArgMatches = ElementMatchers.takesArgument(1, responseTypeName); + ElementMatcher.Junction methodMatches = ElementMatchers.named("service").and(hasTwoArgs.and(firstArgMatches.and(secondArgMatches))); + return methodMatches.and(ElementMatchers.not(ElementMatchers.isAbstract())); + } } diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/WebSupportTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/WebSupportTests.java new file mode 100644 index 0000000..3d828d5 --- /dev/null +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/WebSupportTests.java @@ -0,0 +1,19 @@ +package software.amazon.disco.agent.web; + +import org.junit.Assert; +import org.junit.Test; +import software.amazon.disco.agent.interception.Installable; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class WebSupportTests { + @Test + public void testWebSupport() { + Collection pkg = new WebSupport().get(); + Set installables = new HashSet<>(); + installables.addAll(pkg); + Assert.assertEquals(7, installables.size()); + } +} diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java index f637556..2080aba 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java @@ -16,8 +16,10 @@ package software.amazon.disco.agent.web.apache.httpclient; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHttpRequest; import software.amazon.disco.agent.concurrent.TransactionContext; import software.amazon.disco.agent.event.Event; import software.amazon.disco.agent.event.EventBus; @@ -45,6 +47,9 @@ import org.junit.Before; import org.junit.Test; import software.amazon.disco.agent.web.apache.source.MockEventBusListener; +import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; +import software.amazon.disco.agent.web.apache.utils.HttpRequestBaseAccessor; +import software.amazon.disco.agent.web.apache.utils.HttpResponseAccessor; import java.io.IOException; import java.lang.reflect.Method; @@ -57,8 +62,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class ApacheHttpClientInterceptorTests { @@ -151,7 +155,7 @@ public void testMethodMatcherFailedOnWrongClass() throws Exception { @Test public void testHeaderReplacement() throws Throwable { - HttpGet get = new HttpGet(URI); + HttpUriRequest get = new InterceptedHttpRequestBase(); get.addHeader("foo", "bar"); get.addHeader("foo", "bar2"); @@ -173,12 +177,11 @@ public void testHeaderReplacement() throws Throwable { */ @Test public void testInterceptorSucceededOnChainedMethods() throws Throwable { - HttpUriRequest request = mock(HttpUriRequest.class); - setUpRequest(request); + HttpUriRequest request = new InterceptedHttpRequestBase(); // Set up victim http client SomeChainedExecuteMethodsHttpClient someHttpClient = new SomeChainedExecuteMethodsHttpClient(); - expectedResponse = new BasicHttpResponse(new ProtocolVersion("protocol", 1, 1), 200, ""); + expectedResponse = new InterceptedBasicHttpResponse(new ProtocolVersion("protocol", 1, 1), 200, ""); ApacheHttpClientInterceptor.intercept(new Object[] {request}, "origin", () -> someHttpClient.execute(request)); @@ -201,17 +204,15 @@ public void testInterceptorSucceededOnChainedMethods() throws Throwable { } /** - * Intercepts {@link SomeChainedExecuteMethodsHttpClient#execute(SomeChainedExecuteMethodsHttpClient.WillThrowExceptionOnExecutionHttpRequest)}, + * Intercepts {@link SomeChainedExecuteMethodsHttpClient#execute(WillThrowExceptionOnExecutionHttpRequest)}, * where throws {@link ApacheHttpClientInterceptorTests#expectedIOException} */ @Test(expected = IOException.class) public void testInterceptorSucceededAndReThrowOnException() throws Throwable { - SomeChainedExecuteMethodsHttpClient.WillThrowExceptionOnExecutionHttpRequest request = mock(SomeChainedExecuteMethodsHttpClient.WillThrowExceptionOnExecutionHttpRequest.class); - setUpRequest(request); - // Set up victim http client - SomeChainedExecuteMethodsHttpClient someHttpClient = new SomeChainedExecuteMethodsHttpClient(); expectedIOException = new IOException(); + WillThrowExceptionOnExecutionHttpRequest request = new WillThrowExceptionOnExecutionHttpRequest(); + SomeChainedExecuteMethodsHttpClient someHttpClient = new SomeChainedExecuteMethodsHttpClient(); try { ApacheHttpClientInterceptor.intercept(new Object[]{request}, "origin", () -> someHttpClient.execute(request)); @@ -233,13 +234,6 @@ public void testInterceptorSucceededAndReThrowOnException() throws Throwable { assertEquals(1, someHttpClient.executeMethodChainingDepth); } } - private static void setUpRequest(final HttpRequest request) { - RequestLine requestLine = mock(RequestLine.class); - - when(request.getRequestLine()).thenReturn(requestLine); - when(requestLine.getUri()).thenReturn(URI); - when(requestLine.getMethod()).thenReturn(METHOD); - } private static void verifyServiceRequestEvent(final HttpServiceDownstreamRequestEvent serviceDownstreamRequestEvent) { assertEquals(METHOD, serviceDownstreamRequestEvent.getMethod()); @@ -421,6 +415,50 @@ public HttpResponse execute(WillThrowExceptionOnExecutionHttpRequest request) th } abstract class SomeClassSuperTypeIsHttpRequest implements HttpRequest { } - abstract class WillThrowExceptionOnExecutionHttpRequest implements HttpRequest {} + } + + public class InterceptedHttpRequestBase extends HttpRequestBase implements HttpRequestBaseAccessor, HttpRequestAccessor { + + @Override + public String getMethod() { + return METHOD; + } + + @Override + public String getUri() { + return URI; + } + + @Override + public String getMethodFromRequestLine() { + return null; + } + + @Override + public String getUriFromRequestLine() { + return null; + } + } + + public class WillThrowExceptionOnExecutionHttpRequest extends InterceptedHttpRequestBase { + } + + /** + * A subclass of BasicHttpResponse which pretends that interception occurred, and hence also implements the Accessor + */ + public class InterceptedBasicHttpResponse extends BasicHttpResponse implements HttpResponseAccessor { + public InterceptedBasicHttpResponse(final ProtocolVersion ver, final int code, final String reason) { + super(ver, code, reason); + } + + @Override + public int getStatusCode() { + return super.getStatusLine().getStatusCode(); + } + + @Override + public long getContentLength() { + return 0; + } } } \ No newline at end of file diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/HttpRequestAccessorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/HttpRequestAccessorTests.java deleted file mode 100644 index b60c10e..0000000 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/HttpRequestAccessorTests.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.agent.web.apache.utils; - -import org.apache.http.HttpRequest; -import org.apache.http.RequestLine; -import org.apache.http.client.methods.HttpUriRequest; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class HttpRequestAccessorTests { - - HttpRequest httpRequest; - RequestLine requestLine; - HttpRequestAccessor httpRequestAccessor; - - @Before - public void before() { - httpRequest = mock(HttpRequest.class); - requestLine = mock(RequestLine.class); - httpRequestAccessor = new HttpRequestAccessor(httpRequest); - } - - @Test - public void testAddHeader() throws Throwable { - String headerName = "headerName"; - String headerValue = "headerValue"; - httpRequestAccessor.addHeader(headerName, headerValue); - - ArgumentCaptor headerNameArgumentCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor headerValueArgumentCaptor = ArgumentCaptor.forClass(String.class); - verify(httpRequest, times(1)).addHeader(headerNameArgumentCaptor.capture(), headerValueArgumentCaptor.capture()); - - assertEquals(headerName, headerNameArgumentCaptor.getValue()); - assertEquals(headerValue, headerValueArgumentCaptor.getValue()); - } - - @Test - public void testRemoveHeaders() throws Throwable { - String headerName = "headerName"; - httpRequestAccessor.removeHeaders(headerName); - - ArgumentCaptor headerNameArgumentCaptor = ArgumentCaptor.forClass(String.class); - verify(httpRequest, times(1)).removeHeaders(headerNameArgumentCaptor.capture()); - - assertEquals(headerName, headerNameArgumentCaptor.getValue()); - } - - /** - * Expect returning the FIRST object that is an instance of HttpRequest or its child classes. - */ - @Test - public void testFindRequestObjectFound() { - HttpUriRequest httpUriRequest = mock(HttpUriRequest.class); - - assertEquals(httpRequest, HttpRequestAccessor.findRequestObject("not me", httpRequest, httpUriRequest)); - assertEquals(httpUriRequest, HttpRequestAccessor.findRequestObject("not me", httpUriRequest, httpRequest)); - } - - @Test - public void testFindRequestObjectNotFound() { - assertNull(HttpRequestAccessor.findRequestObject("not me")); - } - - @Test - public void testGetMethodSucceeded() { - String method = "GET"; - - when(requestLine.getMethod()).thenReturn(method); - when(httpRequest.getRequestLine()).thenReturn(requestLine); - - assertEquals(method, httpRequestAccessor.getMethod()); - } - - @Test - public void testGetUriSucceeded() { - String uri = "http://amazon.com/explore/something"; - - when(requestLine.getUri()).thenReturn(uri); - when(httpRequest.getRequestLine()).thenReturn(requestLine); - - assertEquals(uri, httpRequestAccessor.getUri()); - } - - @Test - public void testGetMethodAndGetUriFailed() { - when(httpRequest.getRequestLine()).thenReturn(null); - - assertNull(httpRequestAccessor.getMethod()); - assertNull(httpRequestAccessor.getUri()); - } -} \ No newline at end of file diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/HttpResponseAccessorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/HttpResponseAccessorTests.java deleted file mode 100644 index c471006..0000000 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/HttpResponseAccessorTests.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.agent.web.apache.utils; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mockito; - -public class HttpResponseAccessorTests { - private HttpResponse response; - private HttpResponseAccessor accessor; - - @Before - public void before() { - response = Mockito.mock(HttpResponse.class); - accessor = new HttpResponseAccessor(response); - } - - @Test - public void testGetStatusCode() { - StatusLine statusLine = Mockito.mock(StatusLine.class); - Mockito.when(statusLine.getStatusCode()).thenReturn(404); - Mockito.when(response.getStatusLine()).thenReturn(statusLine); - - Assert.assertEquals(404, accessor.getStatusCode()); - } - - @Test - public void testGetContentLength() { - HttpEntity entity = Mockito.mock(HttpEntity.class); - Mockito.when(entity.getContentLength()).thenReturn(1234L); - Mockito.when(response.getEntity()).thenReturn(entity); - - Assert.assertEquals(1234L, accessor.getContentLength()); - } -} diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletInterceptorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletInterceptorTests.java index 3d8e8e8..c74c1e0 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletInterceptorTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletInterceptorTests.java @@ -157,7 +157,7 @@ private int countMatches(String methodName, Class paramType) throws NoSuchMethod * @return true if matches else false */ private boolean classMatches(Class clazz) throws ClassNotFoundException { - HttpServletInterceptor interceptor = new HttpServletInterceptor() { + HttpServletServiceInterceptor interceptor = new HttpServletServiceInterceptor() { }; return interceptor.buildClassMatcher().matches(new TypeDescription.ForLoadedType(clazz)); } diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletRequestAccessorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletRequestAccessorTests.java index 5077a38..80cd256 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletRequestAccessorTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletRequestAccessorTests.java @@ -27,19 +27,18 @@ import static org.mockito.Mockito.*; public class HttpServletRequestAccessorTests { - HttpServletRequest req; HttpServletRequestAccessor accessor; @Before public void before() { - req = mock(HttpServletRequest.class); - accessor = new HttpServletRequestAccessor(req); + accessor = mock(HttpServletRequestAccessor.class); + when(accessor.retrieveHeaderMap()).thenCallRealMethod(); } @Test public void testGetHeaders() { - when(req.getHeaderNames()).thenReturn(Collections.enumeration(Arrays.asList("headername"))); - when(req.getHeader("headername")).thenReturn("headervalue"); + when(accessor.getHeaderNames()).thenReturn(Collections.enumeration(Arrays.asList("headername"))); + when(accessor.getHeader("headername")).thenReturn("headervalue"); Map map = accessor.retrieveHeaderMap(); Assert.assertEquals(1, map.size()); Assert.assertEquals("headervalue", map.get("headername")); @@ -47,44 +46,44 @@ public void testGetHeaders() { @Test public void testGetHeadersWhenNull() { - when(req.getHeaderNames()).thenReturn(null); + when(accessor.getHeaderNames()).thenReturn(null); Map map = accessor.retrieveHeaderMap(); Assert.assertEquals(0, map.size()); } @Test public void testGetRemotePort() { - when(req.getRemotePort()).thenReturn(800); + when(accessor.getRemotePort()).thenReturn(800); Assert.assertEquals(800, accessor.getRemotePort()); } @Test public void testGetRemoteAddr() { - when(req.getRemoteAddr()).thenReturn("1.2.3.4"); + when(accessor.getRemoteAddr()).thenReturn("1.2.3.4"); Assert.assertEquals("1.2.3.4", accessor.getRemoteAddr()); } @Test public void testGetLocalPort() { - when(req.getLocalPort()).thenReturn(900); + when(accessor.getLocalPort()).thenReturn(900); Assert.assertEquals(900, accessor.getLocalPort()); } @Test public void testGetLocalAddr() { - when(req.getLocalAddr()).thenReturn("4.3.2.1"); + when(accessor.getLocalAddr()).thenReturn("4.3.2.1"); Assert.assertEquals("4.3.2.1", accessor.getLocalAddr()); } @Test public void testGetMethod() { - when(req.getMethod()).thenReturn("POST"); + when(accessor.getMethod()).thenReturn("POST"); Assert.assertEquals("POST", accessor.getMethod()); } @Test public void testGetRequestURL() { - when(req.getRequestURL()).thenReturn(new StringBuffer("http://example.com/foo?bar=baz")); - Assert.assertEquals("http://example.com/foo?bar=baz", accessor.getRequestURL()); + when(accessor.getRequestUrl()).thenReturn("http://example.com/foo?bar=baz"); + Assert.assertEquals("http://example.com/foo?bar=baz", accessor.getRequestUrl()); } } diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletResponseAccessorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletResponseAccessorTests.java index 079ab69..7d91682 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletResponseAccessorTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletResponseAccessorTests.java @@ -27,19 +27,18 @@ import static org.mockito.Mockito.*; public class HttpServletResponseAccessorTests { - HttpServletResponse req; HttpServletResponseAccessor accessor; @Before public void before() { - req = mock(HttpServletResponse.class); - accessor = new HttpServletResponseAccessor(req); + accessor = mock(HttpServletResponseAccessor.class); + when(accessor.retrieveHeaderMap()).thenCallRealMethod(); } @Test public void testGetHeaders() { - when(req.getHeaderNames()).thenReturn(Arrays.asList("headername")); - when(req.getHeader("headername")).thenReturn("headervalue"); + when(accessor.getHeaderNames()).thenReturn(Arrays.asList("headername")); + when(accessor.getHeader("headername")).thenReturn("headervalue"); Map map = accessor.retrieveHeaderMap(); Assert.assertEquals(1, map.size()); Assert.assertEquals("headervalue", map.get("headername")); @@ -47,14 +46,14 @@ public void testGetHeaders() { @Test public void testGetHeadersWhenNull() { - when(req.getHeaderNames()).thenReturn(null); + when(accessor.getHeaderNames()).thenReturn(null); Map map = accessor.retrieveHeaderMap(); Assert.assertEquals(0, map.size()); } @Test public void testGetStatus() { - when(req.getStatus()).thenReturn(202); + when(accessor.getStatus()).thenReturn(202); Assert.assertEquals(202, accessor.getStatus()); } } diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptorTests.java index 2bf7520..f543c45 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptorTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptorTests.java @@ -33,7 +33,9 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -68,8 +70,10 @@ public void before() { EventBus.addListener(testListener = new TestListener()); testServlet = new ImplementedServlet(); - request = Mockito.mock(HttpServletRequest.class); - response = Mockito.mock(HttpServletResponse.class); + request = Mockito.mock(HttpServletRequest.class, Mockito.withSettings().extraInterfaces(HttpServletRequestAccessor.class)); + Mockito.when(((HttpServletRequestAccessor)request).retrieveHeaderMap()).thenCallRealMethod(); + response = Mockito.mock(HttpServletResponse.class, Mockito.withSettings().extraInterfaces(HttpServletResponseAccessor.class)); + Mockito.when(((HttpServletResponseAccessor)response).retrieveHeaderMap()).thenCallRealMethod(); // Custom header List headerNames = new ArrayList<>(); diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/MethodHandleWrapper.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/MethodHandleWrapper.java index 64196b5..ad6a1bb 100644 --- a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/MethodHandleWrapper.java +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/MethodHandleWrapper.java @@ -24,7 +24,11 @@ * Generally we need to access methods reflectively, to account for times when the Agent is built such that it * is loaded into the Bootstrap classloader, which will not have direct access to classes loaded by the System classloader * such as Apache classes, Servlet classes, ... + * + * @deprecated deprecated in favour of {@code software.amazon.disco.agent.interception.templates.DataAccessor} which should be used + * wherever possible instead. */ +@Deprecated public class MethodHandleWrapper { static final MethodHandles.Lookup LOOKUP = MethodHandles.publicLookup(); diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java index 9a98b31..a8c12e7 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java @@ -37,8 +37,6 @@ public class InterceptionInstaller { private static final InterceptionInstaller INSTANCE = new InterceptionInstaller(); private static final Logger log = LogManager.getLogger(InterceptionInstaller.class); - private Set alreadyInstalled = new HashSet(); - /** * Private constructor for singleton semantics */ @@ -85,14 +83,7 @@ public void install(Instrumentation instrumentation, Set installabl } log.info("DiSCo(Core) attempting to install "+installable.getClass().getName()); - if (alreadyInstalled.contains(installable.getClass())) { - log.info("DiSCo(Core)" + installable.getClass().getName() + " already installed; skipping."); - continue; - } - - alreadyInstalled.add(installable.getClass()); agentBuilder = installable.install(agentBuilder); - if (agentBuilder != null) { agentBuilder.installOn(instrumentation); } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java index 5cd2c05..14c44a6 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java @@ -135,8 +135,7 @@ public void testPluginPackageInstallableNonBootstrap() throws Exception { @Test public void testPluginBootstrapFlag() throws Exception { createJar("plugin_with_bootstrap_true", - "Disco-Bootstrap-Classloader: true", - null); + "Disco-Bootstrap-Classloader: true"); Collection outcomes = scanAndApply(instrumentation, agentConfig); Mockito.verify(instrumentation).appendToBootstrapClassLoaderSearch(Mockito.any()); Assert.assertTrue(outcomes.iterator().next().bootstrap); diff --git a/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts b/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts index c73f98b..6804a28 100644 --- a/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts @@ -19,6 +19,8 @@ dependencies { compile("org.ow2.asm", "asm", "7.1") compile("org.ow2.asm", "asm-commons", "7.1") compile("org.ow2.asm", "asm-tree", "7.1") + + testCompile("junit", "junit", "4.12") } configure { diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/annotations/DataAccessPath.java b/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/annotations/DataAccessPath.java new file mode 100644 index 0000000..8d59279 --- /dev/null +++ b/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/annotations/DataAccessPath.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.interception.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Methods in a data accessor annotated with DataAccessPath access their data according to that path. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@java.lang.annotation.Target(ElementType.METHOD) +public @interface DataAccessPath { + + /** + * A path representing the chain of calls required to extract the data being accessed. + * For example the value "getFoo()/getBar()" would call getFoo() on the interception target, and then call getBar() + * on the result of the getFoo() call (without having to know its type). The result produced at the end of this chain + * of calls, if any, is then provided as the return value of the instrumented method. + * + * @return the data path + */ + String value(); +} diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/templates/DataAccessor.java b/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/templates/DataAccessor.java new file mode 100644 index 0000000..331d96b --- /dev/null +++ b/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/templates/DataAccessor.java @@ -0,0 +1,204 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.interception.templates; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.implementation.MethodCall; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; +import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.agent.interception.annotations.DataAccessPath; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Deque; +import java.util.LinkedList; +import java.util.StringTokenizer; + +/** + * A generic template for creating efficient reflection-free accessors onto Objects for interceptors, where no compile-time dependency on the Object's class can + * safely be taken (which is most or all of the time). + * + * This mechanism may make mocking the target objects harder in tests, because the inserted Accessor interface will also have its methods mocked. Test authors + * may prefer to construct real objects instead of mocks. + */ +public class DataAccessor implements Installable { + final ElementMatcher typeImplementingAccessor; + final ElementMatcher typesImplementingAccessMethods; + final Class accessor; + + /** + * Protected constructor for factory access + * @param typeImplementingAccessor ElementMatcher describing the class to implement the accessor, usually some lowest-common denominator base or interface + * @param accessor Accessor interface declaring the methods to use for access + */ + protected DataAccessor(ElementMatcher typeImplementingAccessor, ElementMatcher typesImplementingAccessMethods, Class accessor) { + if (!accessor.isInterface()) { + throw new IllegalArgumentException(); + } + + this.typeImplementingAccessor = typeImplementingAccessor; + this.typesImplementingAccessMethods = typesImplementingAccessMethods; + this.accessor = accessor; + } + + /** + * Factory method to create a DataAccessor which allows access to corresponding methods of the given exact class + * @param className the exact className to project data access onto + * @param accessor the accessor defining the access methods + * @return a constructed DataAccessor + */ + public static DataAccessor forClassNamed(String className, Class accessor) { + ElementMatcher typeMatcher = ElementMatchers.named(className); + return new DataAccessor(typeMatcher, typeMatcher, accessor); + } + + /** + * Factory method to create a DataAccessor which allows access to the corresponding methods of any concrete subclasses of the given interface + * @param interfaceName the name of the interface, for which implementing classes will have access methods defined + * @param accessor the accessor defining the access methods + * @return a constructed DataAccessor + */ + public static DataAccessor forConcreteSubclassesOfInterface(String interfaceName, Class accessor) { + ElementMatcher typeMatcher = ElementMatchers.named(interfaceName).and(ElementMatchers.isInterface()); + return new DataAccessor( + typeMatcher, + ElementMatchers.hasSuperType(typeMatcher).and(ElementMatchers.not(ElementMatchers.isAbstract())), + accessor + ); + } + + /** + * {@inheritDoc} + */ + @Override + public AgentBuilder install(AgentBuilder agentBuilder) { + //first, have the target type(s) implement the accessor + agentBuilder = agentBuilder.type(typeImplementingAccessor).transform((builder, typeDescription, classLoader, module) -> builder.implement(accessor)); + + //now add methods where needed to satisfy DataAccessPath chains + agentBuilder = agentBuilder.type(typesImplementingAccessMethods).transform((builder, typeDescription, classLoader, module) -> { + for (Method method: accessor.getDeclaredMethods()) { + builder = processAccessorMethod(builder, method); + } + + return builder; + } + + ); + + return agentBuilder; + } + + /** + * Instrument the target type(s) as necessary to acquire the data access semantics of the access method + * @param builder the current DynamicType Builder instance for building interception rules + * @param method the method being implemented for the accessor + */ + static DynamicType.Builder processAccessorMethod(DynamicType.Builder builder, Method method) { + //inspect any annotations it might have, otherwise it is a 'simple' form (i.e. primitive types only, perfectly matching the target method in name and signature) + //if annotations present, synthesize the access method + if (method.isAnnotationPresent(DataAccessPath.class)) { + DataAccessPath dap = method.getAnnotation(DataAccessPath.class); + builder = builder + .define(method) + .intercept(Advice.to(ExceptionSafety.class) + .wrap(chainMethodCall(produceCallChain(dap.value())))) + ; + + //the method must not collide with a method already present in the target type, or its superclasses/interfaces + //we should check for this. For now, it's the responsibility of the user, undefined behavior etc. + } + + return builder; + } + + /** + * Produce an ordered collection, in this case a Deque, representing the chain of calls expressed by a DataAccessPath annotation + * @param path the 'path' of chained method calls expressed in a DataAccessPath annotation, of a form like "getFoo()/getBar()" + * @return a Deque of the constituent parts of the path + */ + static Deque produceCallChain(String path) { + StringTokenizer tokenizer = new StringTokenizer(path, "/"); + Deque callChain = new LinkedList<>(); + while (tokenizer.hasMoreTokens()) { + String call = tokenizer.nextToken(); + callChain.push(call); + } + return callChain; + } + + /** + * From a collection of method call instructions e.g. ["getFoo()", "getBar()"], produce a MethodCall implementation joining them + * representing, in pseudo-Java, 'return getFoo().getBar();' + * @param callChain the Deque of call chain parts + * @return a MethodCall implementation representing all the calls chained together + */ + static MethodCall chainMethodCall(Deque callChain) { + //TODO handle param passing. + /* will look something like: + StringTokenizer params = new StringTokenizer(call, "(,)"); + while (params.hasMoreTokens()) { + String param = params.nextToken(); + int paramIndex = Integer.parseInt(param); + Type paramType = method.getParameters()[paramIndex].getType(); + + //produce the next method call with a 'withArgument()' call on the supplied argument + } + */ + //TODO for now, just assume methods taking no params + MethodCall next = produceNextMethodCall(callChain); + while (!callChain.isEmpty()) { + next = produceNextMethodCall(callChain).onMethodCall(next); + } + + return next; + } + + /** + * Produce the next chained method call in a chain of them, by creating a single MethodCall of the next one, applying it to + * the accumulating chain so far + * @param callChain the remaining call chain to be processed + * @return the next chained method call of the complete chain + */ + static MethodCall.WithoutSpecifiedTarget produceNextMethodCall(Deque callChain) { + String s = callChain.removeLast(); + String call = s.substring(0, s.indexOf('(')); + ElementMatcher methodDescriptionElementMatcher = ElementMatchers.named(call); + + return MethodCall.invoke(methodDescriptionElementMatcher); + } + + /** + * An Advice class to wrap generated methods in catch-all exception safety. Should any exception occur, such as a NullPointerException + * from unchecked method chaining, it will be silently suppressed, and the return value of the access method will be the default + * value for the return type i.e. 0 for primitive numbers, and null for reference types. + */ + public static class ExceptionSafety { + /** + * Advice on method exit, to rewrite any thrown exception to be null, and therefore suppressed + * @param thrown the exception which was thrown, which will be suppressed + */ + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void onMethodExit(@Advice.Thrown(readOnly = false) Throwable thrown) { + thrown = null; + } + } +} diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/DataAccessorTests.java b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/DataAccessorTests.java new file mode 100644 index 0000000..a976ad3 --- /dev/null +++ b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/DataAccessorTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.interception.templates; + +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.implementation.MethodCall; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; +import software.amazon.disco.agent.interception.annotations.DataAccessPath; +import software.amazon.disco.agent.interception.templates.integtest.ExampleAccessor; +import software.amazon.disco.agent.interception.templates.integtest.ExampleAccessorInstaller; +import software.amazon.disco.agent.interception.templates.integtest.source.ExampleDelegatedClass; +import software.amazon.disco.agent.interception.templates.integtest.source.ExampleOuterClass; + +import java.lang.reflect.Method; +import java.util.Deque; + +import static net.bytebuddy.matcher.ElementMatchers.named; + +public class DataAccessorTests { + private static final String path = "getFoo()/getBar()/getBaz()"; + + @BeforeClass + public static void beforeClass() { + ExampleAccessorInstaller.init(); + } + + @Test + public void testForClassNamed() { + DataAccessor da = DataAccessor.forClassNamed("software.amazon.disco.agent.interception.templates.DataAccessorTests$TestClass", DummyAccessor.class); + Assert.assertTrue(da.typeImplementingAccessor.matches(new TypeDescription.ForLoadedType(TestClass.class))); + Assert.assertTrue(da.typesImplementingAccessMethods.matches(new TypeDescription.ForLoadedType(TestClass.class))); + } + + @Test + public void testForConcreteSubclassesOfInterface() { + DataAccessor da = DataAccessor.forConcreteSubclassesOfInterface("software.amazon.disco.agent.interception.templates.DataAccessorTests$TestInterface", DummyAccessor.class); + Assert.assertTrue(da.typeImplementingAccessor.matches(new TypeDescription.ForLoadedType(TestInterface.class))); + Assert.assertFalse(da.typesImplementingAccessMethods.matches(new TypeDescription.ForLoadedType(TestBaseClass.class))); + Assert.assertTrue(da.typesImplementingAccessMethods.matches(new TypeDescription.ForLoadedType(TestDerivedClass.class))); + } + + @Test + public void testProcessAccessorMethodWithoutDataPath() throws Exception { + DynamicType.Builder builder = Mockito.mock(DynamicType.Builder.class); + DataAccessor.processAccessorMethod(builder, DummyAccessor.class.getDeclaredMethod("simpleMethod")); + Mockito.verifyNoInteractions(builder); + } + + @Test + public void testProcessAccessorMethodWithDataPath() throws Exception { + DynamicType.Builder builder = Mockito.mock(DynamicType.Builder.class); + DynamicType.Builder.MethodDefinition.ImplementationDefinition implementationDefinition = Mockito.mock(DynamicType.Builder.MethodDefinition.ImplementationDefinition.class); + Mockito.when(builder.define(Mockito.any(Method.class))).thenReturn(implementationDefinition); + DataAccessor.processAccessorMethod(builder, DummyAccessor.class.getDeclaredMethod("pathMethod")); + Mockito.verify(builder).define(Mockito.any(Method.class)); + } + + @Test + public void testProduceCallChain() { + Deque callChain = DataAccessor.produceCallChain(path); + Assert.assertEquals(3, callChain.size()); + Assert.assertEquals("getBaz()", callChain.pop()); + Assert.assertEquals("getBar()", callChain.pop()); + Assert.assertEquals("getFoo()", callChain.pop()); + } + + @Test + public void testProduceNextMethodCall() { + Deque callChain = DataAccessor.produceCallChain(path); + MethodCall methodCall = DataAccessor.produceNextMethodCall(callChain); + MethodCall shouldBe = MethodCall.invoke(named("getFoo")); + Assert.assertEquals(shouldBe, methodCall); + } + + @Test + public void testChainMethodCall() { + Deque callChain = DataAccessor.produceCallChain(path); + MethodCall methodCall = DataAccessor.chainMethodCall(callChain); + MethodCall shouldBe = MethodCall.invoke(named("getBaz")).onMethodCall(MethodCall.invoke(named("getBar")).onMethodCall(MethodCall.invoke(named("getFoo")))); + Assert.assertEquals(shouldBe, methodCall); + } + + // + // Integ tests + // + @Test + public void testAccessor() { + ExampleOuterClass example = new ExampleOuterClass(new ExampleDelegatedClass()); + ExampleAccessor accessor = (ExampleAccessor)example; + Assert.assertEquals("Outer", accessor.getValue()); + Assert.assertEquals("Delegated", accessor.getDelegatedValue()); + Assert.assertEquals(42, accessor.getDelegatedIntValue()); + } + + @Test + public void testAccessorNullPointerHandling() { + ExampleOuterClass example = new ExampleOuterClass(null); + ExampleAccessor accessor = (ExampleAccessor)example; + Assert.assertEquals("Outer", accessor.getValue()); + Assert.assertEquals(null, accessor.getDelegatedValue()); + Assert.assertEquals(0, accessor.getDelegatedIntValue()); + } + + + interface DummyAccessor { + void simpleMethod(); + + @DataAccessPath("foo()/bar()") + void pathMethod(); + } + + static class TestClass {} + + interface TestInterface {} + static abstract class TestBaseClass implements TestInterface {} + static class TestDerivedClass extends TestBaseClass {} +} diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessor.java b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessor.java new file mode 100644 index 0000000..5e41429 --- /dev/null +++ b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessor.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.interception.templates.integtest; + +import software.amazon.disco.agent.interception.annotations.DataAccessPath; + +public interface ExampleAccessor { + String getValue(); + + @DataAccessPath("getDelegate()/getValue()") + String getDelegatedValue(); + + @DataAccessPath("getDelegate()/getIntValue()") + int getDelegatedIntValue(); +} diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessorInstaller.java b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessorInstaller.java new file mode 100644 index 0000000..ca0cf0f --- /dev/null +++ b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessorInstaller.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.interception.templates.integtest; + + +import net.bytebuddy.agent.ByteBuddyAgent; +import net.bytebuddy.agent.builder.AgentBuilder; +import software.amazon.disco.agent.interception.templates.DataAccessor; + +/** + * Install an example DataAccessor for the purposes of an 'interception happened' integ test. + */ +public class ExampleAccessorInstaller { + /** + * Must be called before any use of the ExampleOuterClass or ExampleDelegatedClass types + */ + public static void init() { + DataAccessor da = DataAccessor.forClassNamed("software.amazon.disco.agent.interception.templates.integtest.source.ExampleOuterClass", ExampleAccessor.class); + AgentBuilder ab = da.install(new AgentBuilder.Default()); + ab.installOn(ByteBuddyAgent.install()); + } +} diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleDelegatedClass.java b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleDelegatedClass.java new file mode 100644 index 0000000..07febf5 --- /dev/null +++ b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleDelegatedClass.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.interception.templates.integtest.source; + +public class ExampleDelegatedClass { + public String getValue() { + return "Delegated"; + } + public int getIntValue() {return 42;} +} diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleOuterClass.java b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleOuterClass.java new file mode 100644 index 0000000..e0de09c --- /dev/null +++ b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleOuterClass.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.interception.templates.integtest.source; + +public class ExampleOuterClass { + private final ExampleDelegatedClass delegatedClass; + + public ExampleOuterClass(ExampleDelegatedClass delegatedClass) { + this.delegatedClass = delegatedClass; + } + + public String getValue() { + return "Outer"; + } + + public ExampleDelegatedClass getDelegate() { + return delegatedClass; + } +} From d6c6f0900c6226e267fa58523fef067b67c619f2 Mon Sep 17 00:00:00 2001 From: Connell Date: Mon, 27 Apr 2020 17:25:58 -0700 Subject: [PATCH 05/45] Bump disco version to 0.9.2 --- README.md | 6 +++--- build.gradle.kts | 2 +- .../amazon/disco/application/example/InjectorTest.java | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7dbc5ba..8323485 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ in your Maven or Gradle builds with e.g: software.amazon.disco disco-java-agent-api - 0.9.1 + 0.9.2 ``` @@ -190,7 +190,7 @@ repositories { mavenLocal() } -compile 'software.amazon.disco:disco-java-agent-api:0.9.1' +compile 'software.amazon.disco:disco-java-agent-api:0.9.2' ``` #### Using Gradle's Kotlin DSL @@ -199,7 +199,7 @@ repositories { mavenLocal() } -compile("software.amazon.disco", "disco-java-agent-api", "0.9.1") +compile("software.amazon.disco", "disco-java-agent-api", "0.9.2") ``` ### Troubleshooting diff --git a/build.gradle.kts b/build.gradle.kts index 9cd45da..176a890 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ tasks { subprojects { apply() - version = "0.9.1" + version = "0.9.2" repositories { mavenCentral() diff --git a/disco-java-agent-example-injector-test/src/test/java/software/amazon/disco/application/example/InjectorTest.java b/disco-java-agent-example-injector-test/src/test/java/software/amazon/disco/application/example/InjectorTest.java index 195c2bc..c3473fa 100644 --- a/disco-java-agent-example-injector-test/src/test/java/software/amazon/disco/application/example/InjectorTest.java +++ b/disco-java-agent-example-injector-test/src/test/java/software/amazon/disco/application/example/InjectorTest.java @@ -21,15 +21,16 @@ import org.junit.BeforeClass; import org.junit.Test; +import java.io.File; import java.util.concurrent.atomic.AtomicBoolean; public class InjectorTest { //comment out this annotation to see the test fail, since the agent will not have been loaded. @BeforeClass public static void beforeClass() throws Exception { - //TODO - this string needs a change every version bump. Read it in smartly instead. - final String jarPath = "../disco-java-agent-example/build/libs/disco-java-agent-example-0.9.1.jar"; - Injector.loadAgent(jarPath, "extraverbose"); + File dir = new File("../disco-java-agent-example/build/libs/"); + File[] files = dir.listFiles((dir1, name) -> name.startsWith("disco-java-agent-example-") && name.endsWith(".jar")); + Injector.loadAgent(files[0].getPath(), "extraverbose"); } /** From 689ec0ea84c1a96f30a871ea22d1277c994b4d14 Mon Sep 17 00:00:00 2001 From: Connell Date: Wed, 29 Apr 2020 10:25:33 -0700 Subject: [PATCH 06/45] Add safety to plugin scanning, to ignore and discovered JAR with no disco manifest content --- .../disco/agent/plugin/PluginDiscovery.java | 14 ++++- .../agent/plugin/PluginDiscoveryTests.java | 51 +++++++++++++++---- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java index afca720..ff7a507 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java @@ -208,7 +208,7 @@ static void processJarFile(Instrumentation instrumentation, File jarFile) throws } Attributes attributes = manifest.getMainAttributes(); - if (attributes == null) { + if (attributes == null || attributes.isEmpty()) { log.info("DiSCo(Core) JAR file found with manifest without any main attributes, skipping this file"); return; } @@ -219,6 +219,18 @@ static void processJarFile(Instrumentation instrumentation, File jarFile) throws String listenerClassNames = attributes.getValue("Disco-Listener-Classes"); String bootstrapClassloader = attributes.getValue("Disco-Bootstrap-Classloader"); + //check that at least one of the attributes is present + boolean isPlugin = + (initClassName != null) + || installableClassNames != null + || listenerClassNames != null + || bootstrapClassloader != null; + + if (!isPlugin) { + log.info("DiSCo(Core) JAR file manifest contains no Disco attributes, skipping this file"); + return; + } + //process the plugin based on the Manifest String pluginName = jarFile.getName(); pluginOutcomes.put(pluginName, new PluginOutcome(pluginName)); diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java index 14c44a6..eaffa87 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java @@ -42,6 +42,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.zip.ZipEntry; @@ -57,12 +58,6 @@ public void before() { agentConfig = new AgentConfigParser().parseCommandLine("pluginPath="+tempFolder.getRoot().getAbsolutePath()); instrumentation = Mockito.mock(Instrumentation.class); installables = new HashSet<>(); - EventBus.removeAllListeners(); - } - - @After - public void after() { - EventBus.removeAllListeners(); } @Test @@ -141,6 +136,40 @@ public void testPluginBootstrapFlag() throws Exception { Assert.assertTrue(outcomes.iterator().next().bootstrap); } + @Test + public void testJarWithoutManifestSafelySkipped() throws Exception { + createJar("jar_without_manifest", null); + JarFile jar = new JarFile(agentConfig.getPluginPath() + "/jar_without_manifest.jar"); + Assert.assertNull(jar.getManifest()); + jar.close(); + Collection outcomes = scanAndApply(instrumentation, agentConfig); + Mockito.verifyNoInteractions(instrumentation); + Assert.assertTrue(outcomes.isEmpty()); + + } + + @Test + public void testManifestWithoutMainAttributesSafelySkipped() throws Exception { + createJar("jar_without_main_attributes", ""); + JarFile jar = new JarFile(agentConfig.getPluginPath() + "/jar_without_main_attributes.jar"); + Assert.assertTrue(jar.getManifest().getMainAttributes().isEmpty()); + jar.close(); + Collection outcomes = scanAndApply(instrumentation, agentConfig); + Mockito.verifyNoInteractions(instrumentation); + Assert.assertTrue(outcomes.isEmpty()); + } + + @Test + public void testManifestWithoutDiscoAttributesSafelySkipped() throws Exception { + createJar("jar_without_disco_attributes", "Foobar: boofar"); + JarFile jar = new JarFile(agentConfig.getPluginPath() + "/jar_without_disco_attributes.jar"); + Assert.assertEquals("boofar", jar.getManifest().getMainAttributes().getValue("Foobar")); + jar.close(); + Collection outcomes = scanAndApply(instrumentation, agentConfig); + Mockito.verifyNoInteractions(instrumentation); + Assert.assertTrue(outcomes.isEmpty()); + } + private Collection scanAndApply(Instrumentation instrumentation, AgentConfig agentConfig) { PluginDiscovery.scan(instrumentation, agentConfig); installables.addAll(PluginDiscovery.processInstallables()); @@ -153,10 +182,12 @@ private void createJar(String name, String manifestContent, String... classNames try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { //write manifest - jarOutputStream.putNextEntry(new ZipEntry("META-INF/")); - jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); - jarOutputStream.write((manifestContent+"\n\n").getBytes()); - jarOutputStream.closeEntry(); + if (manifestContent != null) { + jarOutputStream.putNextEntry(new ZipEntry("META-INF/")); + jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); + jarOutputStream.write((manifestContent + "\n\n").getBytes()); + jarOutputStream.closeEntry(); + } //write class file if (classNames != null) { From c7ba4ac4bb149cb96445ab33e43f9139d6356dd2 Mon Sep 17 00:00:00 2001 From: Sai Siripurapu Date: Tue, 5 May 2020 04:33:22 -0700 Subject: [PATCH 07/45] Removing the Springframework classes from default ignore matchers --- .../amazon/disco/agent/interception/InterceptionInstaller.java | 1 - 1 file changed, 1 deletion(-) diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java index a8c12e7..9d725ed 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java @@ -106,7 +106,6 @@ public static ElementMatcher.Junction createIgnoreMatch //3rd party libraries .or(nameStartsWith("org.jacoco.")) .or(nameStartsWith("org.junit.")) - .or(nameStartsWith("org.springframework.")) .or(nameStartsWith("org.aspectj.")) //disco itself and its internals From 1cb9e06f28f6e41c916ac2f7962fdf1611c5933c Mon Sep 17 00:00:00 2001 From: Connell Date: Tue, 5 May 2020 12:04:11 -0700 Subject: [PATCH 08/45] Upgrade to Gradle 6.3. Use its more nuanced api() and implementation() dependency closures. Also moved Junit dependency to top level to avoid repetition, and upgraded Shadow plugin to latest. --- build.gradle.kts | 9 +++++---- .../build.gradle.kts | 5 ++--- disco-java-agent-example-test/build.gradle.kts | 7 +++---- disco-java-agent-example/build.gradle.kts | 4 ++-- disco-java-agent-web/build.gradle.kts | 9 ++++----- .../disco-java-agent-web-plugin/build.gradle.kts | 9 ++++----- disco-java-agent/disco-java-agent-api/build.gradle.kts | 4 ---- .../disco-java-agent-core/build.gradle.kts | 8 +++----- .../disco-java-agent-inject-api/build.gradle.kts | 8 ++------ .../disco-java-agent-plugin-api/build.gradle.kts | 10 ++++------ disco-java-agent/disco-java-agent/build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 12 files changed, 31 insertions(+), 46 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 176a890..410cec1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,8 +18,8 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar //common features available to the entire project //TODO specify the versions of ByteBuddy and ASM in here, since they are used in a few places. plugins { - id("com.github.johnrengelman.shadow") version "5.1.0" apply false - java + id("com.github.johnrengelman.shadow") version "5.2.0" apply false + `java-library` `maven-publish` } @@ -32,7 +32,7 @@ tasks { } subprojects { - apply() + apply() version = "0.9.2" @@ -41,7 +41,8 @@ subprojects { } dependencies { - testCompile("org.mockito", "mockito-core", "3.+") + testImplementation("junit", "junit", "4.12") + testImplementation("org.mockito", "mockito-core", "3.+") } configure { diff --git a/disco-java-agent-example-injector-test/build.gradle.kts b/disco-java-agent-example-injector-test/build.gradle.kts index 38cc548..7d23f41 100644 --- a/disco-java-agent-example-injector-test/build.gradle.kts +++ b/disco-java-agent-example-injector-test/build.gradle.kts @@ -14,9 +14,8 @@ */ dependencies { - testCompile("junit", "junit", "4.12") - testCompile(project(":disco-java-agent:disco-java-agent-api")) - testCompile(project(":disco-java-agent:disco-java-agent-inject-api", "shadow")) + testImplementation(project(":disco-java-agent:disco-java-agent-api")) + testImplementation(project(":disco-java-agent:disco-java-agent-inject-api", "shadow")) } //make sure the agents and plugins we use have been built, without taking any real dependencies on them diff --git a/disco-java-agent-example-test/build.gradle.kts b/disco-java-agent-example-test/build.gradle.kts index aa25434..e4f5a1c 100644 --- a/disco-java-agent-example-test/build.gradle.kts +++ b/disco-java-agent-example-test/build.gradle.kts @@ -14,10 +14,9 @@ */ dependencies { - testCompile(project(":disco-java-agent:disco-java-agent-api")) - testCompile("junit", "junit", "4.12") - testCompile("javax.servlet", "javax.servlet-api", "3.0.1") - testCompile("org.apache.httpcomponents", "httpclient", "4.5.10") + testImplementation(project(":disco-java-agent:disco-java-agent-api")) + testImplementation("javax.servlet", "javax.servlet-api", "3.0.1") + testImplementation("org.apache.httpcomponents", "httpclient", "4.5.10") } val ver = project.version diff --git a/disco-java-agent-example/build.gradle.kts b/disco-java-agent-example/build.gradle.kts index 3fab865..497bf4b 100644 --- a/disco-java-agent-example/build.gradle.kts +++ b/disco-java-agent-example/build.gradle.kts @@ -18,8 +18,8 @@ plugins { } dependencies { - compile(project(":disco-java-agent:disco-java-agent-core")) - compile(project(":disco-java-agent-web")) + implementation(project(":disco-java-agent:disco-java-agent-core")) + implementation(project(":disco-java-agent-web")) } tasks.shadowJar { diff --git a/disco-java-agent-web/build.gradle.kts b/disco-java-agent-web/build.gradle.kts index 265d0ac..b6b4485 100644 --- a/disco-java-agent-web/build.gradle.kts +++ b/disco-java-agent-web/build.gradle.kts @@ -14,11 +14,10 @@ */ dependencies { - compile(project(":disco-java-agent:disco-java-agent-core")) - testCompile("junit", "junit", "4.12") - testCompile("org.mockito", "mockito-core", "1.+") - testCompile("javax.servlet", "javax.servlet-api", "3.0.1") - testCompile("org.apache.httpcomponents", "httpclient", "4.5.10") + implementation(project(":disco-java-agent:disco-java-agent-core")) + testImplementation("org.mockito", "mockito-core", "1.+") + testImplementation("javax.servlet", "javax.servlet-api", "3.0.1") + testImplementation("org.apache.httpcomponents", "httpclient", "4.5.10") } configure { diff --git a/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts b/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts index 7e6aa1c..3107e0b 100644 --- a/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts +++ b/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts @@ -30,11 +30,10 @@ dependencies { //Test target is integ tests for this plugin. Some classes in the integ tests also self-test via little unit tests during this //testrun. - testCompile(project(":disco-java-agent:disco-java-agent-api")) - testCompile("junit", "junit", "4.12") - testCompile("org.mockito", "mockito-core", "1.+") - testCompile("javax.servlet", "javax.servlet-api", "3.0.1") - testCompile("org.apache.httpcomponents", "httpclient", "4.5.10") + testImplementation(project(":disco-java-agent:disco-java-agent-api")) + testImplementation("org.mockito", "mockito-core", "1.+") + testImplementation("javax.servlet", "javax.servlet-api", "3.0.1") + testImplementation("org.apache.httpcomponents", "httpclient", "4.5.10") } tasks.shadowJar { diff --git a/disco-java-agent/disco-java-agent-api/build.gradle.kts b/disco-java-agent/disco-java-agent-api/build.gradle.kts index 4369f67..ceba2ea 100644 --- a/disco-java-agent/disco-java-agent-api/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-api/build.gradle.kts @@ -13,10 +13,6 @@ * permissions and limitations under the License. */ -dependencies { - testCompile("junit", "junit", "4.12") -} - configure { publications { named("maven") { diff --git a/disco-java-agent/disco-java-agent-core/build.gradle.kts b/disco-java-agent/disco-java-agent-core/build.gradle.kts index 95a4f5b..08a015b 100644 --- a/disco-java-agent/disco-java-agent-core/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-core/build.gradle.kts @@ -14,11 +14,9 @@ */ dependencies { - compile(project(":disco-java-agent:disco-java-agent-plugin-api")) - compile(project(":disco-java-agent:disco-java-agent-inject-api")) - compile(project(":disco-java-agent:disco-java-agent-api")) - - testCompile("junit", "junit", "4.12") + api(project(":disco-java-agent:disco-java-agent-plugin-api")) + api(project(":disco-java-agent:disco-java-agent-inject-api")) + api(project(":disco-java-agent:disco-java-agent-api")) } configure { diff --git a/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts b/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts index 4be1ded..2499612 100644 --- a/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts @@ -20,12 +20,8 @@ plugins { dependencies { //we use the ByteBuddyAgent for an install-after-startup injection strategy, but do not want to inadvertently //pull all of BB into the client's code. - compile("net.bytebuddy", "byte-buddy-agent", "1.9.12") { - isTransitive = false - } - - testCompile("net.bytebuddy", "byte-buddy-dep", "1.9.12") - testCompile("junit", "junit", "4.12") + implementation("net.bytebuddy", "byte-buddy-agent", "1.9.12") + testImplementation("net.bytebuddy", "byte-buddy-dep", "1.9.12") } configure { diff --git a/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts b/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts index 6804a28..a89f8ff 100644 --- a/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts @@ -15,12 +15,10 @@ dependencies { //TODO update BB and ASM to latest - compile("net.bytebuddy", "byte-buddy-dep", "1.9.12") - compile("org.ow2.asm", "asm", "7.1") - compile("org.ow2.asm", "asm-commons", "7.1") - compile("org.ow2.asm", "asm-tree", "7.1") - - testCompile("junit", "junit", "4.12") + api("net.bytebuddy", "byte-buddy-dep", "1.9.12") + implementation("org.ow2.asm", "asm", "7.1") + implementation("org.ow2.asm", "asm-commons", "7.1") + implementation("org.ow2.asm", "asm-tree", "7.1") } configure { diff --git a/disco-java-agent/disco-java-agent/build.gradle.kts b/disco-java-agent/disco-java-agent/build.gradle.kts index ecda4e8..e2dbedb 100644 --- a/disco-java-agent/disco-java-agent/build.gradle.kts +++ b/disco-java-agent/disco-java-agent/build.gradle.kts @@ -18,7 +18,7 @@ plugins { } dependencies { - compile(project(":disco-java-agent:disco-java-agent-core")) + implementation(project(":disco-java-agent:disco-java-agent-core")) } tasks.shadowJar { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4b7e1f3..a4b4429 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 40db775364102f5d0cfcc92a532b2acfe4bf1304 Mon Sep 17 00:00:00 2001 From: Connell Date: Wed, 6 May 2020 10:30:28 -0700 Subject: [PATCH 09/45] Fix gradle warning about forward compatibility with gradle 7.0 --- disco-java-agent/disco-java-agent-core/build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/disco-java-agent/disco-java-agent-core/build.gradle.kts b/disco-java-agent/disco-java-agent-core/build.gradle.kts index 08a015b..04c394d 100644 --- a/disco-java-agent/disco-java-agent-core/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-core/build.gradle.kts @@ -40,11 +40,11 @@ sourceSets { //create a new empty integ test config - not extending from existing compile or testCompile, since we don't want to //be able to compile against Core etc. -val integtestCompile by configurations.getting {} +val integtestImplementation: Configuration by configurations.getting {} dependencies { - integtestCompile("junit", "junit", "4.12") - integtestCompile(project(":disco-java-agent:disco-java-agent-api")) + integtestImplementation("junit", "junit", "4.12") + integtestImplementation(project(":disco-java-agent:disco-java-agent-api")) } val ver = project.version From 1632cd594f700dfccb48f668b7527e575ce053bc Mon Sep 17 00:00:00 2001 From: Connell Date: Wed, 6 May 2020 11:06:42 -0700 Subject: [PATCH 10/45] Remove needless bootstrap injection stuff --- .../interception/InterceptionInstaller.java | 12 +---- .../amazon/disco/agent/utils/FileUtils.java | 51 ------------------- .../disco/agent/utils/FileUtilsTest.java | 34 ------------- 3 files changed, 1 insertion(+), 96 deletions(-) delete mode 100644 disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/utils/FileUtils.java delete mode 100644 disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/utils/FileUtilsTest.java diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java index 9d725ed..d10c4b8 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java @@ -15,7 +15,6 @@ package software.amazon.disco.agent.interception; -import software.amazon.disco.agent.utils.FileUtils; import software.amazon.disco.agent.config.AgentConfig; import software.amazon.disco.agent.logging.LogManager; import software.amazon.disco.agent.logging.Logger; @@ -23,9 +22,7 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; -import java.io.File; import java.lang.instrument.Instrumentation; -import java.util.HashSet; import java.util.Set; import static net.bytebuddy.matcher.ElementMatchers.*; @@ -63,19 +60,12 @@ public void install(Instrumentation instrumentation, Set installabl ElementMatcher.Junction customIgnoreMatcher) { ElementMatcher ignoreMatcher = createIgnoreMatcher(customIgnoreMatcher); - File bootstrapTemp = FileUtils.getInstance().getTempFolder(); - //if the folder has remnants from a previous run, remove them - for (File file : bootstrapTemp.listFiles()) { - file.delete(); - } for (Installable installable: installables) { //We create a new Agent for each Installable, otherwise their matching rules can //compete with each other. AgentBuilder agentBuilder = new AgentBuilder.Default() - .ignore(ignoreMatcher) - .enableBootstrapInjection(instrumentation, bootstrapTemp) - ; + .ignore(ignoreMatcher); //The Interception listener is expensive during class loading, and limited value most of the time if (config.isExtraverbose()) { diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/utils/FileUtils.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/utils/FileUtils.java deleted file mode 100644 index 5333a20..0000000 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/utils/FileUtils.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.agent.utils; - -import java.io.File; - -/** - * Various utilities methods. - */ -public class FileUtils { - private final static FileUtils INSTANCE = new FileUtils(); - - /** - * Private constructor for singleton semantics - */ - private FileUtils() { - } - - /** - * Singleton access - * @return the Utils singleton - */ - public static FileUtils getInstance() { - return INSTANCE; - } - - /** - * Finds a folder for temporary files. - * - * @return the folder where temporary files should be stored. - */ - public File getTempFolder() { - File tmpFolder = new File(System.getProperty("java.io.tmpdir"), "software.amazon.disco.agent"); - if (!tmpFolder.exists()) - tmpFolder.mkdirs(); - return tmpFolder; - } -} diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/utils/FileUtilsTest.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/utils/FileUtilsTest.java deleted file mode 100644 index 13641d8..0000000 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/utils/FileUtilsTest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.agent.utils; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; - -import java.io.File; - -import static org.junit.Assert.assertEquals; - -@RunWith(MockitoJUnitRunner.class) -public class FileUtilsTest { - @Test - public void testFolder() { - File fakeRoot = FileUtils.getInstance().getTempFolder(); - Assert.assertNotNull(fakeRoot.getAbsolutePath()); - } -} \ No newline at end of file From 09475570d2c002aa8e5e4c42c950b10d5d0a3944 Mon Sep 17 00:00:00 2001 From: Connell Date: Tue, 5 May 2020 14:14:35 -0700 Subject: [PATCH 11/45] Allow parameterization in DataAccessPath annotations --- .../annotations/DataAccessPath.java | 19 +++ .../interception/templates/DataAccessor.java | 71 +++++++---- .../templates/DataAccessorTests.java | 120 ++++++++++++++++-- .../templates/integtest/ExampleAccessor.java | 6 + .../source/ExampleDelegatedClass.java | 7 + .../integtest/source/ExampleOuterClass.java | 8 ++ 6 files changed, 196 insertions(+), 35 deletions(-) diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/annotations/DataAccessPath.java b/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/annotations/DataAccessPath.java index 8d59279..824b35f 100644 --- a/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/annotations/DataAccessPath.java +++ b/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/annotations/DataAccessPath.java @@ -34,6 +34,25 @@ * on the result of the getFoo() call (without having to know its type). The result produced at the end of this chain * of calls, if any, is then provided as the return value of the instrumented method. * + * Parameters can also be supplied on the DataAccessPath, when the called methods do not have bean-like getter semantics. + * For example, consider the DataAccessPath required to call Bar::getSomethingByName, given an Accessor created for Bar: + * + * class Bar { + * getSomethingByName(String name); + * } + * + * class Foo { + * getBarByIndex(int index); + * } + * + * class FooAccessor { + * @DataAccessPath("getBarByIndex(0)/getSomethingByName(1)") + * getSomething(int index, String name); + * } + * + * The annotation on getSomething() first calls getBarByIndex with the 0th argument of getSomething(), then, on the + * result of that call (which is an object of type Bar), calls getSomethingByName with the 1st argument of getSomething(). + * * @return the data path */ String value(); diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/templates/DataAccessor.java b/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/templates/DataAccessor.java index 331d96b..659fa12 100644 --- a/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/templates/DataAccessor.java +++ b/disco-java-agent/disco-java-agent-plugin-api/src/main/java/software/amazon/disco/agent/interception/templates/DataAccessor.java @@ -27,7 +27,6 @@ import software.amazon.disco.agent.interception.annotations.DataAccessPath; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.Deque; import java.util.LinkedList; import java.util.StringTokenizer; @@ -98,10 +97,8 @@ public AgentBuilder install(AgentBuilder agentBuilder) { for (Method method: accessor.getDeclaredMethods()) { builder = processAccessorMethod(builder, method); } - return builder; } - ); return agentBuilder; @@ -120,7 +117,7 @@ static DynamicType.Builder processAccessorMethod(DynamicType.Builder build builder = builder .define(method) .intercept(Advice.to(ExceptionSafety.class) - .wrap(chainMethodCall(produceCallChain(dap.value())))) + .wrap(chainMethodCall(method, produceCallChain(dap.value())))) ; //the method must not collide with a method already present in the target type, or its superclasses/interfaces @@ -146,27 +143,18 @@ static Deque produceCallChain(String path) { } /** - * From a collection of method call instructions e.g. ["getFoo()", "getBar()"], produce a MethodCall implementation joining them + * From a collection of method call instructions e.g. ["getFoo()", "getBar(0)"], produce a MethodCall implementation joining them * representing, in pseudo-Java, 'return getFoo().getBar();' + * @param accessorMethod the method declared in the accessor * @param callChain the Deque of call chain parts * @return a MethodCall implementation representing all the calls chained together */ - static MethodCall chainMethodCall(Deque callChain) { - //TODO handle param passing. - /* will look something like: - StringTokenizer params = new StringTokenizer(call, "(,)"); - while (params.hasMoreTokens()) { - String param = params.nextToken(); - int paramIndex = Integer.parseInt(param); - Type paramType = method.getParameters()[paramIndex].getType(); - - //produce the next method call with a 'withArgument()' call on the supplied argument - } - */ - //TODO for now, just assume methods taking no params - MethodCall next = produceNextMethodCall(callChain); + static MethodCall chainMethodCall(Method accessorMethod, Deque callChain) { + String callString = callChain.removeLast(); + MethodCall next = produceNextMethodCall(null, accessorMethod, callString); while (!callChain.isEmpty()) { - next = produceNextMethodCall(callChain).onMethodCall(next); + callString = callChain.removeLast(); + next = produceNextMethodCall(next, accessorMethod, callString); } return next; @@ -175,15 +163,46 @@ static MethodCall chainMethodCall(Deque callChain) { /** * Produce the next chained method call in a chain of them, by creating a single MethodCall of the next one, applying it to * the accumulating chain so far - * @param callChain the remaining call chain to be processed + * @param previous the MethodCall to be chained onto + * @param accessorMethod the method declared in the accessor + * @param callString the next call in the call chain to be processed e.g. "getFoo(0)" * @return the next chained method call of the complete chain */ - static MethodCall.WithoutSpecifiedTarget produceNextMethodCall(Deque callChain) { - String s = callChain.removeLast(); - String call = s.substring(0, s.indexOf('(')); - ElementMatcher methodDescriptionElementMatcher = ElementMatchers.named(call); + static MethodCall produceNextMethodCall(MethodCall previous, Method accessorMethod, String callString) { + String call = callString.substring(0, callString.indexOf('(')); + StringTokenizer tokenizer = new StringTokenizer(callString.substring(callString.indexOf('(')), "(),"); + int[] params = new int[tokenizer.countTokens()]; + int index = 0; + while (tokenizer.hasMoreTokens()) { + String token = tokenizer.nextToken(); + params[index++] = Integer.parseInt(token); + } + + MethodCall.WithoutSpecifiedTarget methodCallWithout = MethodCall.invoke(createParameterizedAccessMethodMatcher(call, accessorMethod, params)); + MethodCall methodCall = methodCallWithout; + if (previous != null) { + methodCall = methodCallWithout.onMethodCall(previous); + } + if (params.length > 0) { + methodCall = methodCall.withArgument(params); + } + return methodCall; + } - return MethodCall.invoke(methodDescriptionElementMatcher); + /** + * Helper method to produce an ElementMatcher matching a unique method in the target class, by fully specifying name and argument signature + * @param methodName the name of the method + * @param accessorMethod the accessor's method with a DataAccessPath which is being used to supply arguments, from which we determine type signature + * @param params array of parameter indices into accessorMethod's formal arguments + * @return an ElementMatcher which will uniquely match the method intended to be called. + */ + static ElementMatcher createParameterizedAccessMethodMatcher(String methodName, Method accessorMethod, int[] params) { + ElementMatcher.Junction methodDescriptionElementMatcher = ElementMatchers.named(methodName).and(ElementMatchers.takesArguments(params.length)); + int index = 0; + for (int param: params) { + methodDescriptionElementMatcher = methodDescriptionElementMatcher.and(ElementMatchers.takesArgument(index++, accessorMethod.getParameterTypes()[param])); + } + return methodDescriptionElementMatcher; } /** diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/DataAccessorTests.java b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/DataAccessorTests.java index a976ad3..c4c46f3 100644 --- a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/DataAccessorTests.java +++ b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/DataAccessorTests.java @@ -15,9 +15,11 @@ package software.amazon.disco.agent.interception.templates; +import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.DynamicType; import net.bytebuddy.implementation.MethodCall; +import net.bytebuddy.matcher.ElementMatcher; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -31,10 +33,11 @@ import java.lang.reflect.Method; import java.util.Deque; -import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.*; public class DataAccessorTests { private static final String path = "getFoo()/getBar()/getBaz()"; + private static final String pathWithArgs = "getFoo(2)/getBar(1)/getBaz(0)"; @BeforeClass public static void beforeClass() { @@ -73,7 +76,7 @@ public void testProcessAccessorMethodWithDataPath() throws Exception { } @Test - public void testProduceCallChain() { + public void testProduceCallChainNoArgs() { Deque callChain = DataAccessor.produceCallChain(path); Assert.assertEquals(3, callChain.size()); Assert.assertEquals("getBaz()", callChain.pop()); @@ -82,21 +85,96 @@ public void testProduceCallChain() { } @Test - public void testProduceNextMethodCall() { - Deque callChain = DataAccessor.produceCallChain(path); - MethodCall methodCall = DataAccessor.produceNextMethodCall(callChain); - MethodCall shouldBe = MethodCall.invoke(named("getFoo")); + public void testProduceCallChainWithArgs() { + Deque callChain = DataAccessor.produceCallChain(pathWithArgs); + Assert.assertEquals(3, callChain.size()); + Assert.assertEquals("getBaz(0)", callChain.pop()); + Assert.assertEquals("getBar(1)", callChain.pop()); + Assert.assertEquals("getFoo(2)", callChain.pop()); + } + + @Test + public void testProduceNextMethodCallNoArgs() { + MethodCall methodCall = DataAccessor.produceNextMethodCall(null, null, "getFoo()"); + MethodCall shouldBe = MethodCall.invoke(named("getFoo").and(takesArguments(0))); Assert.assertEquals(shouldBe, methodCall); } @Test - public void testChainMethodCall() { + public void testProduceNextMethodCallWithArgs() throws Exception { + MethodCall methodCall = DataAccessor.produceNextMethodCall(null, this.getClass().getDeclaredMethod("simpleAccessor", int.class), "getFoo(0)"); + MethodCall shouldBe = MethodCall.invoke(named("getFoo").and(takesArguments(1)).and(takesArgument(0, int.class))).withArgument(0); + Assert.assertEquals(shouldBe, methodCall); + } + + @Test + public void testChainMethodCallNoArgs() { Deque callChain = DataAccessor.produceCallChain(path); - MethodCall methodCall = DataAccessor.chainMethodCall(callChain); - MethodCall shouldBe = MethodCall.invoke(named("getBaz")).onMethodCall(MethodCall.invoke(named("getBar")).onMethodCall(MethodCall.invoke(named("getFoo")))); + MethodCall methodCall = DataAccessor.chainMethodCall(null, callChain); + MethodCall shouldBe = MethodCall.invoke( + named("getBaz").and(takesArguments(0))) + .onMethodCall(MethodCall.invoke(named("getBar").and(takesArguments(0))) + .onMethodCall(MethodCall.invoke(named("getFoo").and(takesArguments(0))))); + Assert.assertEquals(shouldBe, methodCall); + } + + @Test + public void testChainMethodCallWithArgs() throws Exception { + Deque callChain = DataAccessor.produceCallChain(pathWithArgs); + MethodCall methodCall = DataAccessor.chainMethodCall(this.getClass().getDeclaredMethod("chainedAccessor", int.class, int.class, int.class), callChain); + + MethodCall shouldBe = MethodCall.invoke( + named("getBaz").and(takesArguments(1)).and(takesArgument(0, int.class))) + .onMethodCall(MethodCall.invoke(named("getBar").and(takesArguments(1)).and(takesArgument(0, int.class))) + .onMethodCall(MethodCall.invoke(named("getFoo").and(takesArguments(1)).and(takesArgument(0, int.class))) + .withArgument(2)).withArgument(1)).withArgument(0); + Assert.assertEquals(shouldBe, methodCall); } + @Test + public void testMethodMatcherUniquelyMatches() throws Exception { + testUniqueness(); + testUniqueness(int.class); + testUniqueness(Object.class); + testUniqueness(String.class); + } + + //helper for matcher uniqueness test + private void testUniqueness(Class... parameterType) throws Exception { + Method method = MatcherTester.class.getDeclaredMethod("foo", parameterType); + int[] params; + if (parameterType.length == 0) { + params = new int[0]; + } else { + params = new int[]{0}; + } + ElementMatcher matcher = DataAccessor.createParameterizedAccessMethodMatcher("foo", method , params); + for (Method m: MatcherTester.class.getDeclaredMethods()) { + if (m.equals(method)) { + Assert.assertTrue(matcher.matches(new MethodDescription.ForLoadedMethod(m))); + } else { + Assert.assertFalse(matcher.matches(new MethodDescription.ForLoadedMethod(m))); + } + } + } + + //class with many 'nearly matching' methods, to stress the uniqueness of the parameterized method matcher + interface MatcherTester { + void foo(); + void foo(int i); + void foo(Object o); + void foo(String s); + } + + //private methods accessed reflectively to pass to produceNextMethodCall() + private Object simpleAccessor(int i) { + return null; + } + private String chainedAccessor(int i, int j, int k) { + return null; + } + // // Integ tests // @@ -109,6 +187,23 @@ public void testAccessor() { Assert.assertEquals(42, accessor.getDelegatedIntValue()); } + @Test + public void testAccessorWithParam() { + ExampleOuterClass example = new ExampleOuterClass(new ExampleDelegatedClass()); + ExampleAccessor accessor = (ExampleAccessor)example; + ExampleDelegatedClass delegatedClass = (ExampleDelegatedClass)accessor.getDelegateByKey(42); + Assert.assertEquals("Delegated", delegatedClass.getValue()); + Assert.assertNull(accessor.getDelegateByKey(41)); + } + + @Test + public void testAccessorWithChainedParams() { + ExampleOuterClass example = new ExampleOuterClass(new ExampleDelegatedClass()); + ExampleAccessor accessor = (ExampleAccessor)example; + ExampleDelegatedClass delegatedClass = (ExampleDelegatedClass)accessor.getDelegateByKey(42); + Assert.assertEquals("Delegated", accessor.getDelegatedValueByKeyAndSecret(42, "secret")); + } + @Test public void testAccessorNullPointerHandling() { ExampleOuterClass example = new ExampleOuterClass(null); @@ -118,6 +213,13 @@ public void testAccessorNullPointerHandling() { Assert.assertEquals(0, accessor.getDelegatedIntValue()); } + @Test + public void testAccessorNullPointerHandlingWithParams() { + ExampleOuterClass example = new ExampleOuterClass(null); + ExampleAccessor accessor = (ExampleAccessor)example; + Assert.assertEquals(null, accessor.getDelegatedValueByKeyAndSecret(42, "secret")); + } + interface DummyAccessor { void simpleMethod(); diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessor.java b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessor.java index 5e41429..a5d3514 100644 --- a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessor.java +++ b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/ExampleAccessor.java @@ -25,4 +25,10 @@ public interface ExampleAccessor { @DataAccessPath("getDelegate()/getIntValue()") int getDelegatedIntValue(); + + @DataAccessPath("getDelegate(0)") + Object getDelegateByKey(int key); + + @DataAccessPath("getDelegate(0)/getValue(1)") + String getDelegatedValueByKeyAndSecret(int key, String secret); } diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleDelegatedClass.java b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleDelegatedClass.java index 07febf5..c83ac53 100644 --- a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleDelegatedClass.java +++ b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleDelegatedClass.java @@ -20,4 +20,11 @@ public String getValue() { return "Delegated"; } public int getIntValue() {return 42;} + public String getValue(String secret) { + if ("secret".equals(secret)) { + return getValue(); + } else { + return null; + } + } } diff --git a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleOuterClass.java b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleOuterClass.java index e0de09c..a72a929 100644 --- a/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleOuterClass.java +++ b/disco-java-agent/disco-java-agent-plugin-api/src/test/java/software/amazon/disco/agent/interception/templates/integtest/source/ExampleOuterClass.java @@ -29,4 +29,12 @@ public String getValue() { public ExampleDelegatedClass getDelegate() { return delegatedClass; } + + public ExampleDelegatedClass getDelegate(int key) { + if (key == 42) { + return delegatedClass; + } else { + return null; + } + } } From 0b22167516ec7fccae00674534eb5c56542cf129 Mon Sep 17 00:00:00 2001 From: Sai Siripurapu Date: Mon, 11 May 2020 16:07:11 -0700 Subject: [PATCH 12/45] Adding a method to MethodHandleWrapper to provide functionality to check whether MethodHandle is loaded --- .../agent/reflect/MethodHandleWrapper.java | 9 ++++ .../reflect/MethodHandleWrapperTests.java | 51 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/MethodHandleWrapperTests.java diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/MethodHandleWrapper.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/MethodHandleWrapper.java index ad6a1bb..29a1f0a 100644 --- a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/MethodHandleWrapper.java +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/MethodHandleWrapper.java @@ -160,4 +160,13 @@ private static Class[] classesFromClassNames(ClassLoader classLoader, String[] n return classes; } + /** + * Method to provide information whether MethodHandle is loaded or not + * eg: returns false if any exceptions are thrown while building MethodHandle + * @return the {@link Boolean} + */ + public boolean isHandleLoaded() { + return handle != null; + } + } diff --git a/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/MethodHandleWrapperTests.java b/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/MethodHandleWrapperTests.java new file mode 100644 index 0000000..edee536 --- /dev/null +++ b/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/MethodHandleWrapperTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.disco.agent.reflect; + +import org.junit.Assert; +import org.junit.Test; + +public class MethodHandleWrapperTests { + + @Test + public void testMethodFound() { + ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + MethodHandleWrapper handler = new MethodHandleWrapper(Object.class.getCanonicalName(), + classLoader, + "toString", + String.class); + Assert.assertTrue(handler.isHandleLoaded()); + } + + @Test + public void testClassFoundMethodNotFound() { + ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + MethodHandleWrapper handler = new MethodHandleWrapper(Object.class.getCanonicalName(), + classLoader, + "noMethod", + Integer.class); + Assert.assertFalse(handler.isHandleLoaded()); + } + + @Test + public void testClassNotFound() { + ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + MethodHandleWrapper handler = new MethodHandleWrapper("com.FakeClass", + classLoader, + "noMethod", + Integer.class); + Assert.assertFalse(handler.isHandleLoaded()); + } +} From 91a3719467f3da9167b2a5dfee5c8166f84f0f41 Mon Sep 17 00:00:00 2001 From: Arjun Jeyaprakash Date: Thu, 14 May 2020 14:45:20 -0700 Subject: [PATCH 13/45] Add method to remove metadata from TransactionContext map --- .../reflect/concurrent/TransactionContext.java | 16 ++++++++++++++++ .../concurrent/TransactionContextTests.java | 5 ++++- .../agent/concurrent/TransactionContext.java | 14 +++++++++++++- .../concurrent/TransactionContextTests.java | 9 +++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java index e1e79ce..e423d6a 100644 --- a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java @@ -101,6 +101,22 @@ public static void putMetadata(String key, Object value) { call.call(key, value); } + /** + * Remove a value in the DiSCo metadata map, or do nothing if the agent is not loaded + * @param key the key of the metadata + */ + public static void removeMetadata(String key) { + Logger.info("Removing metadata with key " + key); + ReflectiveCall call = ReflectiveCall.returningVoid() + .ofClass(TRANSACTIONCONTEXT_CLASS) + .ofMethod("removeMetadata") + .withArgTypes(String.class); + + checkMetadataKey(call, key); + + call.call(key); + } + /** * Get a value from the DiSCo metadata map, or null if the agent is not loaded * @param key the key of the metadata diff --git a/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/concurrent/TransactionContextTests.java b/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/concurrent/TransactionContextTests.java index 1b06dd6..f7f67b1 100644 --- a/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/concurrent/TransactionContextTests.java +++ b/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/concurrent/TransactionContextTests.java @@ -44,7 +44,10 @@ public void testTransactionContextSetMetadataNoAgentLoaded() { TransactionContext.putMetadata("metadata", "value"); Assert.assertNull(TransactionContext.getMetadata("metadata")); } - + @Test + public void testRemoveMetadataDoesNotThrowIfReservedIdentifierWhenAgentNotLoaded() { + TransactionContext.removeMetadata("$amazon.discoIdentifier"); + } @Test public void testTransactionContextGetMetadataNoAgentLoaded() { Assert.assertNull(TransactionContext.getMetadata("metadata")); diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java index 43460df..3a03cf6 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java @@ -117,7 +117,7 @@ public static void set(String value) { /** * Place an arbitrary value into the map - * @param key a String to identify the data. May not begin with "disco" - this prefix is reserved for internal use. + * @param key a String to identify the data. May not be "discoTransactionId" which is reserved internally. * @param value the metadata value */ public static void putMetadata(String key, Object value) { @@ -128,6 +128,18 @@ public static void putMetadata(String key, Object value) { transactionContext.get().put(key, new MetadataItem(value)); } + /** + * Remove a value from the map + * + * @param key a String to identify the data. May not be "discoTransactionId" which is reserved internally. + */ + public static void removeMetadata (String key) { + if (TRANSACTION_ID_KEY.equals(key)) { + throw new IllegalArgumentException(TRANSACTION_ID_KEY + " may not be used as a metadata key"); + } + transactionContext.get().remove(key); + } + /** * Get data from the metadata map * @param key a String to identify the data. May not be "discoTransactionId" which is reserved internally. diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/TransactionContextTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/TransactionContextTests.java index 77a7dfb..8992e42 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/TransactionContextTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/TransactionContextTests.java @@ -152,6 +152,15 @@ public void testMetadata() { Assert.assertEquals("bar", bar); } + @Test + public void testRemoveMetadata() { + TransactionContext.putMetadata("foo", "bar"); + String bar = String.class.cast(TransactionContext.getMetadata("foo")); + Assert.assertEquals("bar", bar); + TransactionContext.removeMetadata("foo"); + Assert.assertNull(TransactionContext.getMetadata("foo")); + } + @Test public void testTaggedMetadata() { TransactionContext.putMetadata("foo1", "bar1"); From e8cdbc4a9a236d27837e7aea7bd08e4708b0ac2b Mon Sep 17 00:00:00 2001 From: Ben Smithers Date: Fri, 5 Jun 2020 13:28:59 +0100 Subject: [PATCH 14/45] Add concurrency support for the ScheduledThreadPoolExecutor --- .../concurrent/ExecutorServiceTests.java | 64 +++++--- .../ScheduledThreadPoolExecutorTests.java | 71 +++++++++ .../source/ExecutorServiceFactory.java | 22 +++ ...FixedThreadPoolExecutorServiceFactory.java | 29 ++++ .../ScheduledThreadPoolExecutorFactory.java | 29 ++++ .../agent/concurrent/ConcurrencySupport.java | 3 +- .../agent/concurrent/ExecutorInterceptor.java | 18 +-- .../agent/concurrent/InterceptorUtils.java | 43 ++++++ ...cheduledThreadPoolExecutorInterceptor.java | 117 +++++++++++++++ .../agent/concurrent/ThreadInterceptor.java | 9 +- .../DecoratedRunnableScheduledFuture.java | 141 ++++++++++++++++++ .../concurrent/ConcurrencySupportTests.java | 3 +- .../concurrent/ExecutorInterceptorTests.java | 6 + ...ledThreadPoolExecutorInterceptorTests.java | 120 +++++++++++++++ .../concurrent/ThreadInterceptorTests.java | 6 +- 15 files changed, 640 insertions(+), 41 deletions(-) create mode 100644 disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ScheduledThreadPoolExecutorTests.java create mode 100644 disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/ExecutorServiceFactory.java create mode 100644 disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/FixedThreadPoolExecutorServiceFactory.java create mode 100644 disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/ScheduledThreadPoolExecutorFactory.java create mode 100644 disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/InterceptorUtils.java create mode 100644 disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ScheduledThreadPoolExecutorInterceptor.java create mode 100644 disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/DecoratedRunnableScheduledFuture.java create mode 100644 disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ScheduledThreadPoolExecutorInterceptorTests.java diff --git a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java index 0f56df9..c7b389f 100644 --- a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java +++ b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java @@ -15,11 +15,7 @@ package software.amazon.disco.agent.integtest.concurrent; -import software.amazon.disco.agent.integtest.concurrent.source.ForceConcurrency; -import software.amazon.disco.agent.integtest.concurrent.source.TestableConcurrencyObject; -import software.amazon.disco.agent.integtest.concurrent.source.TestableConcurrencyObjectImpl; -import software.amazon.disco.agent.integtest.concurrent.source.TestCallableFactory; -import software.amazon.disco.agent.integtest.concurrent.source.TestRunnableFactory; +import software.amazon.disco.agent.integtest.concurrent.source.*; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -28,20 +24,20 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collectors; /** - * Test that our Runnable/Callable strategy works for a Java Executor, a common concurrency mechanism. + * Test that our Runnable/Callable strategy works for Java Executors, a common concurrency mechanism. */ public class ExecutorServiceTests { + + private static final List executorServiceFactories = Arrays.asList( + new FixedThreadPoolExecutorServiceFactory(), + new ScheduledThreadPoolExecutorFactory() + ); + static abstract class Base { protected ExecutorService executorService; protected static final String result = "Result"; @@ -49,7 +45,6 @@ static abstract class Base { @Before public void before() { TestableConcurrencyObjectImpl.before(); - executorService = Executors.newFixedThreadPool(2); } @After @@ -76,8 +71,20 @@ public static abstract class RunnableBase extends Base { @Parameterized.Parameter(1) public TestRunnableFactory.TestableRunnable testableRunnable; + @Parameterized.Parameter(2) + public ExecutorServiceFactory executorServiceFactory; + @Parameterized.Parameters(name="{0}") - public static Collection data() {return TestRunnableFactory.Data.provideAllRunnables();} + public static Collection data() { + return executorServiceFactories.stream() + .flatMap(e -> + TestRunnableFactory.Data.provideAllRunnables().stream() + .map(data -> { + String name = String.format("runnable:%s executor:%s", data[0], e.getClass().getSimpleName()); + return new Object[]{name, data[1], e}; + })) + .collect(Collectors.toList()); + } } public static abstract class CallableBase extends Base { @@ -87,8 +94,20 @@ public static abstract class CallableBase extends Base { @Parameterized.Parameter(1) public TestCallableFactory.NonThrowingTestableCallable testableCallable; + @Parameterized.Parameter(2) + public ExecutorServiceFactory executorServiceFactory; + @Parameterized.Parameters(name="{0}") - public static Collection data() {return TestCallableFactory.Data.provideAllCallables();} + public static Collection data() { + return executorServiceFactories.stream() + .flatMap(e -> + TestCallableFactory.Data.provideAllCallables().stream() + .map(data -> { + String name = String.format("callable:%s executor:%s", data[0], e.getClass().getSimpleName()); + return new Object[]{name, data[1], e}; + })) + .collect(Collectors.toList()); + } } @RunWith(Parameterized.class) @@ -98,6 +117,7 @@ public static class ExecuteRunnable extends RunnableBase { @Test public void testExecuteRunnable() throws Exception { + executorService = executorServiceFactory.createExecutorService(); testBeforeInvocation(testableRunnable); executorService.execute(testableRunnable.getRunnable()); testAfterInvocation(testableRunnable, null, null); @@ -111,6 +131,7 @@ public static class SubmitRunnable extends RunnableBase { @Test public void testSubmitRunnable() throws Exception { + executorService = executorServiceFactory.createExecutorService(); testBeforeInvocation(testableRunnable); Future future = executorService.submit(testableRunnable.getRunnable()); testAfterInvocation(testableRunnable, future.get(), null); @@ -124,6 +145,7 @@ public static class SubmitRunnableWithResult extends RunnableBase { @Test public void testSubmitRunnableWithResult() throws Exception { + executorService = executorServiceFactory.createExecutorService(); testBeforeInvocation(testableRunnable); Future future = executorService.submit(testableRunnable.getRunnable(), result); testAfterInvocation(testableRunnable, future.get(), result); @@ -137,6 +159,7 @@ public static class SubmitCallable extends CallableBase { @Test public void testSubmitCallable() throws Exception { + executorService = executorServiceFactory.createExecutorService(); testBeforeInvocation(testableCallable); Future future = executorService.submit(testableCallable.callable); testAfterInvocation(testableCallable, future.get(), result); @@ -150,6 +173,7 @@ public static class InvokeAllCallable extends CallableBase { @Test public void testInvokeAllCallable() throws Exception { + executorService = executorServiceFactory.createExecutorService(); testBeforeInvocation(testableCallable); List> callables = new LinkedList<>(); callables.add(testableCallable.callable); @@ -165,6 +189,7 @@ public static class InvokeAllCallableWithTimeout extends CallableBase { @Test public void testInvokeAllCallableWithTimeout() throws Exception { + executorService = executorServiceFactory.createExecutorService(); testBeforeInvocation(testableCallable); List> callables = new LinkedList<>(); callables.add(testableCallable.callable); @@ -180,6 +205,7 @@ public static class InvokeAnyCallable extends CallableBase { @Test public void testInvokeAnyCallable() throws Exception { + executorService = executorServiceFactory.createExecutorService(); testBeforeInvocation(testableCallable); List> callables = new LinkedList<>(); callables.add(testableCallable.callable); @@ -195,6 +221,7 @@ public static class InvokeAnyCallableWithTimeout extends CallableBase { @Test public void testInvokeAnyCallableWithTimeout() throws Exception { + executorService = executorServiceFactory.createExecutorService(); testBeforeInvocation(testableCallable); List> callables = new LinkedList<>(); callables.add(testableCallable.callable); @@ -211,6 +238,7 @@ class SubmitRunnableWhenThrows extends Base { public void testSubmitRunnableWhenThrows() throws Exception { ThrowingRunnable r = new ThrowingRunnable(); r.testBeforeInvocation(); + executorService = Executors.newFixedThreadPool(2); Future f = executorService.submit(r); executorService.shutdown(); diff --git a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ScheduledThreadPoolExecutorTests.java b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ScheduledThreadPoolExecutorTests.java new file mode 100644 index 0000000..1e56728 --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ScheduledThreadPoolExecutorTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.integtest.concurrent; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.disco.agent.integtest.concurrent.source.TestCallableFactory; +import software.amazon.disco.agent.integtest.concurrent.source.TestRunnableFactory; + +import java.util.Collection; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +// Specific tests for the additional public methods of the ScheduledThreadPoolExecutor +public class ScheduledThreadPoolExecutorTests { + @RunWith(Parameterized.class) + public static class ScheduleRunnable extends ExecutorServiceTests.Base { + + @Parameterized.Parameter(0) + public String name; + + @Parameterized.Parameter(1) + public TestRunnableFactory.TestableRunnable testableRunnable; + + @Parameterized.Parameters(name="{0}") + public static Collection data() {return TestRunnableFactory.Data.provideAllRunnables();} + + @Test + public void testScheduleRunnable() throws Exception { + executorService = new ScheduledThreadPoolExecutor(2); + testBeforeInvocation(testableRunnable); + ((ScheduledThreadPoolExecutor) executorService).schedule(testableRunnable.getRunnable(), 1, TimeUnit.MILLISECONDS); + testAfterInvocation(testableRunnable, null, null); + } + } + + @RunWith(Parameterized.class) + public static class ScheduleCallable extends ExecutorServiceTests.Base { + + @Parameterized.Parameter(0) + public String name; + + @Parameterized.Parameter(1) + public TestCallableFactory.NonThrowingTestableCallable testableCallable; + + @Parameterized.Parameters(name="{0}") + public static Collection data() {return TestCallableFactory.Data.provideAllCallables();} + + @Test + public void testScheduleCallable() throws Exception { + executorService = new ScheduledThreadPoolExecutor(2); + testBeforeInvocation(testableCallable); + ((ScheduledThreadPoolExecutor) executorService).schedule(testableCallable.callable, 1, TimeUnit.MILLISECONDS); + testAfterInvocation(testableCallable, null, null); + } + } +} diff --git a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/ExecutorServiceFactory.java b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/ExecutorServiceFactory.java new file mode 100644 index 0000000..fd47ff0 --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/ExecutorServiceFactory.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.integtest.concurrent.source; + +import java.util.concurrent.ExecutorService; + +public interface ExecutorServiceFactory { + ExecutorService createExecutorService(); +} diff --git a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/FixedThreadPoolExecutorServiceFactory.java b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/FixedThreadPoolExecutorServiceFactory.java new file mode 100644 index 0000000..c1e73e2 --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/FixedThreadPoolExecutorServiceFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.disco.agent.integtest.concurrent.source; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Factory to create a fixed thread pool of 2 threads. A pool size >1 may be required for tests that ensure a thread hand-off occurred, see + * {@link software.amazon.disco.agent.integtest.concurrent.source.TestableConcurrencyObjectImpl#testAfterConcurrentInvocation()} + */ +public class FixedThreadPoolExecutorServiceFactory implements ExecutorServiceFactory { + @Override + public ExecutorService createExecutorService() { + return Executors.newFixedThreadPool(2); + } +} diff --git a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/ScheduledThreadPoolExecutorFactory.java b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/ScheduledThreadPoolExecutorFactory.java new file mode 100644 index 0000000..8f302b4 --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/ScheduledThreadPoolExecutorFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.disco.agent.integtest.concurrent.source; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +/** + * Factory to create a ScheduledThreadPoolExecutor with a pool size of 2. A pool size >1 may be required for tests that ensure a thread + * hand-off occurred, see {@link software.amazon.disco.agent.integtest.concurrent.source.TestableConcurrencyObjectImpl#testAfterConcurrentInvocation()} + */ +public class ScheduledThreadPoolExecutorFactory implements ExecutorServiceFactory { + @Override + public ExecutorService createExecutorService() { + return new ScheduledThreadPoolExecutor(2); + } +} diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ConcurrencySupport.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ConcurrencySupport.java index 0c6f7e2..d4dabc2 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ConcurrencySupport.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ConcurrencySupport.java @@ -35,7 +35,8 @@ public Collection get() { new ForkJoinPoolInterceptor(), new ForkJoinTaskInterceptor(), new ThreadInterceptor(), - new ThreadSubclassInterceptor() + new ThreadSubclassInterceptor(), + new ScheduledThreadPoolExecutorInterceptor() ); } } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ExecutorInterceptor.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ExecutorInterceptor.java index 7f96a14..a287f05 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ExecutorInterceptor.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ExecutorInterceptor.java @@ -26,6 +26,7 @@ import net.bytebuddy.matcher.ElementMatcher; import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledThreadPoolExecutor; import static net.bytebuddy.matcher.ElementMatchers.*; @@ -45,21 +46,15 @@ class ExecutorInterceptor implements Installable { */ @Override public AgentBuilder install(AgentBuilder agentBuilder) { - return agentBuilder - //As with the similar code in ThreadInterceptor, we handle situations where given Executors - //may already have been used e.g. we know for sure that AspectJ has use-cases where it instantiates - //a ThreadPoolExecutor. This gives DiSCo a responsibility to adopt an interception strategy where - //we can transform a class that is already loaded. The code below achieves that. - .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) - .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) - .with(AgentBuilder.TypeStrategy.Default.REDEFINE) - + // Configure redefinition to handle situations where given Executors may already have been used e.g. we know for + // sure that AspectJ has use-cases where it instantiates a ThreadPoolExecutor. This gives DiSCo a responsibility + // to adopt an interception strategy where we can transform a class that is already loaded + return InterceptorUtils.configureRedefinition(agentBuilder) .type(createTypeMatcher()) .transform((builder, typeDescription, classLoader, module) -> builder .visit(Advice.to(ExecuteAdvice.class) .on(createMethodMatcher())) ); - } /** @@ -107,7 +102,8 @@ public static void captureThrowableForDebugging (Throwable t) { * @return a type matcher as above */ static ElementMatcher.Junction createTypeMatcher() { - return isSubTypeOf(Executor.class); + return isSubTypeOf(Executor.class) + .and(not(isSubTypeOf(ScheduledThreadPoolExecutor.class))); // Handled separately //TODO should we exclude ForkJoinPool here, due to it being an impl of Executor, but handled elsewhere? } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/InterceptorUtils.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/InterceptorUtils.java new file mode 100644 index 0000000..0b3c090 --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/InterceptorUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.disco.agent.concurrent; + +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * Utility functions for Interceptors. + */ +public class InterceptorUtils { + + /** + * Private constructor, use static methods directly. + */ + private InterceptorUtils() { + + } + + /** + * Configure the AgentBuilder to allow redefinition of an already-loaded class + * + * @param agentBuilder the AgentBuilder + * @return the AgentBuilder, configured to redefine an already-loaded class + */ + public static AgentBuilder configureRedefinition(AgentBuilder agentBuilder) { + return agentBuilder + .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) + .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) + .with(AgentBuilder.TypeStrategy.Default.REDEFINE); + } +} diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ScheduledThreadPoolExecutorInterceptor.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ScheduledThreadPoolExecutorInterceptor.java new file mode 100644 index 0000000..00b7076 --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ScheduledThreadPoolExecutorInterceptor.java @@ -0,0 +1,117 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.concurrent; + +import software.amazon.disco.agent.concurrent.decorate.DecoratedRunnableScheduledFuture; +import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.agent.logging.LogManager; +import software.amazon.disco.agent.logging.Logger; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import java.util.concurrent.RunnableScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +import static net.bytebuddy.matcher.ElementMatchers.*; + +/** + * An interceptor for the {@link ScheduledThreadPoolExecutor} to provide TransactionContext propagation. + * + * The {@link ScheduledThreadPoolExecutor} is a special case of Executor that provides additional + * methods for submitting work (schedule and friends) and wraps Runnables and Callables in a + * {@link RunnableScheduledFuture}, preventing the standard ExecutorInterceptor from handling this case. The + * ScheduledThreadPoolExecutor however provides decorateTask methods that can be intercepted for ThreadContext + * propagation. + */ +class ScheduledThreadPoolExecutorInterceptor implements Installable { + private static Logger log = LogManager.getLogger(ScheduledThreadPoolExecutorInterceptor.class); + + /** + * {@inheritDoc} + */ + @Override + public AgentBuilder install(AgentBuilder agentBuilder) { + return InterceptorUtils.configureRedefinition(agentBuilder) + .type(createTypeMatcher()) + .transform((builder, typeDescription, classLoader, module) -> builder + .visit(Advice.to(DecorateTaskAdvice.class) + .on(createMethodMatcher())) + ); + } + + /** + * Advice class to decorate the decorateTask() method of a ScheduledThreadPoolExecutor + */ + public static class DecorateTaskAdvice { + + /** + * Advice method to decorate the RunnableScheduledFuture on entry to decorateTask. + * + * @param task the actual task executed by the ScheduledThreadPoolExecutor + */ + @Advice.OnMethodEnter + public static void onMethodEnter(@Advice.Argument(value = 1, readOnly = false) RunnableScheduledFuture task) { + try { + task = decorate(task); + } catch (Throwable t) { + captureThrowableForDebugging(t); + } + } + + /** + * A trampoline method to make debugging possible from within an Advice. + * + * @param task the RunnableScheduledFuture to decorate + * @return a decorated RunnableScheduledFuture + */ + public static RunnableScheduledFuture decorate(RunnableScheduledFuture task) { + return DecoratedRunnableScheduledFuture.maybeCreate(task); + } + + /** + * Under normal circumstances should not be called, but for debugging, we call out to a 'real' method + * @param t the throwable which was thrown by the advice + */ + public static void captureThrowableForDebugging (Throwable t) { + log.error("DiSCo(Concurrency) failed to decorate RunnableScheduledFuture for ScheduledThreadPoolExecutor", t); + } + } + + /** + * Create a type matcher for a ScheduledThreadPoolExecutor or any subclass. + * + * @return the type matcher + */ + static ElementMatcher.Junction createTypeMatcher() { + return isSubTypeOf(ScheduledThreadPoolExecutor.class); + } + + /** + * Create a method matcher to match the decorateTask method (accepting either Callable or Runnable, we don't care). + * + * Note that this method is protected, but exists specifically for interception and inspection of the work being + * submitted to this Executor. + * + * @return a method matcher + */ + static ElementMatcher.Junction createMethodMatcher() { + return named("decorateTask"); + } +} + diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ThreadInterceptor.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ThreadInterceptor.java index 41e9170..bf67642 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ThreadInterceptor.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ThreadInterceptor.java @@ -44,14 +44,9 @@ class ThreadInterceptor implements Installable { */ @Override public AgentBuilder install(AgentBuilder agentBuilder) { - return agentBuilder + //redefinition required, because Thread is already loaded at this point (we are in the main thread!) + return InterceptorUtils.configureRedefinition(agentBuilder) .ignore(noneOf(Thread.class)) - - //redefinition required, because Thread is already loaded at this point (we are in the main thread!) - .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) - .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) - .with(AgentBuilder.TypeStrategy.Default.REDEFINE) - .type(createThreadTypeMatcher()) .transform((builder, typeDescription, classLoader, module) -> builder //the familiar idiom of method().intercept() does not work for Thread diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/DecoratedRunnableScheduledFuture.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/DecoratedRunnableScheduledFuture.java new file mode 100644 index 0000000..9c67837 --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/DecoratedRunnableScheduledFuture.java @@ -0,0 +1,141 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.concurrent.decorate; + +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.RunnableScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Given a RunnableScheduledFuture object, decorate it with thread-info metadata to allow propagation of DiSCo + * TransactionContext across thread hand-off in a ScheduledThreadPoolExecutor. + */ +public class DecoratedRunnableScheduledFuture extends Decorated implements RunnableScheduledFuture { + + private final RunnableScheduledFuture target; + + /** + * Create a new DecoratedRunnableScheduledFuture. Private, use factory to create. + * + * @param target the RunnableScheduledFuture to decorate + */ + private DecoratedRunnableScheduledFuture(final RunnableScheduledFuture target) { + this.target = target; + } + + /** + * Factory method to decorate a RunnableScheduledFuture only if it is not already a DecoratedRunnableScheduledFuture. + * + * @param target the RunnableScheduledFuture to consider for decoration + * @return a DecoratedRunnableScheduledFuture representing the input RunnableScheduledFuture + */ + public static RunnableScheduledFuture maybeCreate(RunnableScheduledFuture target) { + if (target == null) { + return null; + } + + if (target instanceof DecoratedRunnableScheduledFuture) { + return target; + } + + return new DecoratedRunnableScheduledFuture(target); + } + + /** + * When the RunnableScheduledFuture is executed, perform DiSCo TransactionContext propagation, as necessary + * + * {@inheritDoc} + */ + @Override + public void run() { + before(); + try { + target.run(); + } catch (Throwable t) { + throw t; + } finally { + after(); + } + } + + // Delegate all abstract methods + + /** + * {@inheritDoc} + */ + @Override + public int compareTo(Delayed o) { + return target.compareTo(o); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPeriodic() { + return this.target.isPeriodic(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isDone() { + return this.target.isDone(); + } + + /** + * {@inheritDoc} + */ + @Override + public long getDelay(TimeUnit unit) { + return this.target.getDelay(unit); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return this.target.cancel(mayInterruptIfRunning); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isCancelled() { + return this.target.isCancelled(); + } + + /** + * {@inheritDoc} + */ + @Override + public V get() throws InterruptedException, ExecutionException { + return this.target.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return this.target.get(timeout, unit); + } +} diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ConcurrencySupportTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ConcurrencySupportTests.java index 363b81a..eff42b4 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ConcurrencySupportTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ConcurrencySupportTests.java @@ -25,11 +25,12 @@ public class ConcurrencySupportTests { @Test public void testPackageContentCorrect() { List installables = (List)new ConcurrencySupport().get(); - Assert.assertEquals(5, installables.size()); + Assert.assertEquals(6, installables.size()); Assert.assertEquals(ExecutorInterceptor.class, installables.get(0).getClass()); Assert.assertEquals(ForkJoinPoolInterceptor.class, installables.get(1).getClass()); Assert.assertEquals(ForkJoinTaskInterceptor.class, installables.get(2).getClass()); Assert.assertEquals(ThreadInterceptor.class, installables.get(3).getClass()); Assert.assertEquals(ThreadSubclassInterceptor.class, installables.get(4).getClass()); + Assert.assertEquals(ScheduledThreadPoolExecutorInterceptor.class, installables.get(5).getClass()); } } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ExecutorInterceptorTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ExecutorInterceptorTests.java index 02a100f..eeb6aba 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ExecutorInterceptorTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ExecutorInterceptorTests.java @@ -29,6 +29,7 @@ import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.Executor; import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ScheduledThreadPoolExecutor; public class ExecutorInterceptorTests { ElementMatcher typeMatcher = ExecutorInterceptor.createTypeMatcher(); @@ -49,6 +50,11 @@ public void testTypeMatcherMatchesConcrete() { Assert.assertTrue(typeMatcher.matches(new TypeDescription.ForLoadedType(ForkJoinPool.class))); } + @Test + public void testTypeMatcherDoesNotMatchScheduledThreadPoolExecutor() { + Assert.assertFalse(typeMatcher.matches(new TypeDescription.ForLoadedType(ScheduledThreadPoolExecutor.class))); + } + @Test public void testMethodMatcherNotMatchesInterface() throws Exception { Method execute = Executor.class.getDeclaredMethod("execute", Runnable.class); diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ScheduledThreadPoolExecutorInterceptorTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ScheduledThreadPoolExecutorInterceptorTests.java new file mode 100644 index 0000000..5c1ff3a --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ScheduledThreadPoolExecutorInterceptorTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.concurrent; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import software.amazon.disco.agent.concurrent.decorate.DecoratedRunnableScheduledFuture; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; +import java.util.concurrent.RunnableScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +public class ScheduledThreadPoolExecutorInterceptorTests { + + private static class TestSubclass extends ScheduledThreadPoolExecutor { + public TestSubclass() { + super(1); + } + + @Override + protected RunnableScheduledFuture decorateTask(Runnable runnable, RunnableScheduledFuture task) { + return super.decorateTask(runnable, task); + } + + @Override + protected RunnableScheduledFuture decorateTask(Callable callable, RunnableScheduledFuture task) { + return super.decorateTask(callable, task); + } + } + + @Test + public void testThatTypeMatcherMatches() { + Assert.assertTrue(ScheduledThreadPoolExecutorInterceptor.createTypeMatcher() + .matches(new TypeDescription.ForLoadedType(ScheduledThreadPoolExecutor.class))); + } + + @Test + public void testThatTypeMatcherMatchesSubclasses() { + Assert.assertTrue(ScheduledThreadPoolExecutorInterceptor.createTypeMatcher() + .matches(new TypeDescription.ForLoadedType(TestSubclass.class))); + } + + @Test + public void testThatMethodMatchesWithCallable() throws NoSuchMethodException { + Method m = ScheduledThreadPoolExecutor.class.getDeclaredMethod("decorateTask", Callable.class, + RunnableScheduledFuture.class); + + Assert.assertTrue(ScheduledThreadPoolExecutorInterceptor.createMethodMatcher() + .matches(new MethodDescription.ForLoadedMethod(m))); + } + + @Test + public void testThatMethodMatchesWithRunnable() throws NoSuchMethodException { + Method m = ScheduledThreadPoolExecutor.class.getDeclaredMethod("decorateTask", Runnable.class, + RunnableScheduledFuture.class); + + Assert.assertTrue(ScheduledThreadPoolExecutorInterceptor.createMethodMatcher() + .matches(new MethodDescription.ForLoadedMethod(m))); + } + + @Test + public void testThatMethodMatchesWithCallableOnSubclass() throws NoSuchMethodException { + Method m = TestSubclass.class.getDeclaredMethod("decorateTask", Callable.class, + RunnableScheduledFuture.class); + + Assert.assertTrue(ScheduledThreadPoolExecutorInterceptor.createMethodMatcher() + .matches(new MethodDescription.ForLoadedMethod(m))); + } + + @Test + public void testThatMethodMatchesWithRunnableOnSubclass() throws NoSuchMethodException { + Method m = TestSubclass.class.getDeclaredMethod("decorateTask", Runnable.class, + RunnableScheduledFuture.class); + + Assert.assertTrue(ScheduledThreadPoolExecutorInterceptor.createMethodMatcher() + .matches(new MethodDescription.ForLoadedMethod(m))); + } + + @Test + public void testInstall() { + AgentBuilder agentBuilder = Mockito.mock(AgentBuilder.class); + AgentBuilder.RedefinitionListenable.WithoutBatchStrategy withoutBatchStrategy = Mockito.mock(AgentBuilder.RedefinitionListenable.WithoutBatchStrategy.class); + AgentBuilder.Identified.Extendable extendable = Mockito.mock(AgentBuilder.Identified.Extendable.class); + AgentBuilder.Identified.Narrowable narrowable = Mockito.mock(AgentBuilder.Identified.Narrowable.class); + Mockito.when(agentBuilder.with(Mockito.any(AgentBuilder.InitializationStrategy.class))).thenReturn(agentBuilder); + Mockito.when(agentBuilder.with(Mockito.any(AgentBuilder.RedefinitionStrategy.class))).thenReturn(withoutBatchStrategy); + Mockito.when(withoutBatchStrategy.with(Mockito.any(AgentBuilder.TypeStrategy.class))).thenReturn(agentBuilder); + Mockito.when(agentBuilder.type(Mockito.any(ElementMatcher.class))).thenReturn(narrowable); + Mockito.when(narrowable.transform(Mockito.any(AgentBuilder.Transformer.class))).thenReturn(extendable); + AgentBuilder result = new ScheduledThreadPoolExecutorInterceptor().install(agentBuilder); + Assert.assertEquals(extendable, result); + } + + @Test + public void testThatAdviceDecoratesTask() { + RunnableScheduledFuture scheduledFuture = Mockito.mock(RunnableScheduledFuture.class); + RunnableScheduledFuture result = ScheduledThreadPoolExecutorInterceptor.DecorateTaskAdvice.decorate(scheduledFuture); + Assert.assertTrue(result instanceof DecoratedRunnableScheduledFuture); + } + +} diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ThreadInterceptorTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ThreadInterceptorTests.java index 0bba812..7f97984 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ThreadInterceptorTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ThreadInterceptorTests.java @@ -61,10 +61,10 @@ public void testInstall() throws Exception { AgentBuilder.Ignored ignored = Mockito.mock(AgentBuilder.Ignored.class); AgentBuilder.RedefinitionListenable.WithoutBatchStrategy withoutBatchStrategy = Mockito.mock(AgentBuilder.RedefinitionListenable.WithoutBatchStrategy.class); AgentBuilder.Identified.Narrowable narrowable = Mockito.mock(AgentBuilder.Identified.Narrowable.class); + Mockito.when(agentBuilder.with(Mockito.any(AgentBuilder.InitializationStrategy.class))).thenReturn(agentBuilder); + Mockito.when(agentBuilder.with(Mockito.any(AgentBuilder.RedefinitionStrategy.class))).thenReturn(withoutBatchStrategy); + Mockito.when(withoutBatchStrategy.with(Mockito.any(AgentBuilder.TypeStrategy.class))).thenReturn(agentBuilder); Mockito.when(agentBuilder.ignore(Mockito.any(ElementMatcher.class))).thenReturn(ignored); - Mockito.when(ignored.with(Mockito.any(AgentBuilder.InitializationStrategy.class))).thenReturn(ignored); - Mockito.when(ignored.with(Mockito.any(AgentBuilder.RedefinitionStrategy.class))).thenReturn(withoutBatchStrategy); - Mockito.when(withoutBatchStrategy.with(Mockito.any(AgentBuilder.TypeStrategy.class))).thenReturn(ignored); Mockito.when(ignored.type(Mockito.any(ElementMatcher.class))).thenReturn(narrowable); new ThreadInterceptor().install(agentBuilder); } From 9b9a25595d28624c2cc822837de20a706a34a9e4 Mon Sep 17 00:00:00 2001 From: Sai Siripurapu Date: Mon, 15 Jun 2020 12:17:29 -0700 Subject: [PATCH 15/45] Http Servlet Interceptor re-enterant logic change from transaction context size to context key check. --- .../HttpServletServiceInterceptor.java | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptor.java index b7f5ced..7e1e779 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/servlet/HttpServletServiceInterceptor.java @@ -42,6 +42,7 @@ public class HttpServletServiceInterceptor implements Installable { private final static Logger log = LogManager.getLogger(HttpServletServiceInterceptor.class); + private static final String TX_NAMESPACE = "HTTP_SERVLET_SERVICE"; private static final String EVENT_ORIGIN = "httpServlet"; // Common HTTP Header keys @@ -87,23 +88,28 @@ public static void service(@AllArguments Object[] args, if (LogManager.isDebugEnabled()) { log.debug("DiSCo(Web) interception of " + origin); } - int txStackDepth = TransactionContext.create(); - - //since service() calls in subclasses may call their parents, this interceptor can stack up - //only perform event publication it if we were the first call to take place - if (txStackDepth == 0) { - try { - // To reduce the # of dependencies, we use reflection to obtain the basic methods. - Object request = args[0]; - HttpServletRequestAccessor reqAccessor = (HttpServletRequestAccessor)request; - - // Obtain the metadata information from the host. - // If they are null, they are't stored, so retrieval would be null as well. - int srcPort = reqAccessor.getRemotePort(); - int dstPort = reqAccessor.getLocalPort(); - String srcIP = reqAccessor.getRemoteAddr(); - String dstIP = reqAccessor.getLocalAddr(); - requestEvent = new HttpServletNetworkRequestEvent(EVENT_ORIGIN, srcPort, dstPort, srcIP, dstIP) + + if (TransactionContext.isWithinCreatedContext() && TransactionContext.getMetadata(TX_NAMESPACE) != null) { + //since service() calls in subclasses may call their parents, this interceptor can stack up + //only perform event publication it if we were the first call to take place + zuper.call(); + return; + } + TransactionContext.create(); + TransactionContext.putMetadata(TX_NAMESPACE, true); + + try { + // To reduce the # of dependencies, we use reflection to obtain the basic methods. + Object request = args[0]; + HttpServletRequestAccessor reqAccessor = (HttpServletRequestAccessor) request; + + // Obtain the metadata information from the host. + // If they are null, they are't stored, so retrieval would be null as well. + int srcPort = reqAccessor.getRemotePort(); + int dstPort = reqAccessor.getLocalPort(); + String srcIP = reqAccessor.getRemoteAddr(); + String dstIP = reqAccessor.getLocalAddr(); + requestEvent = new HttpServletNetworkRequestEvent(EVENT_ORIGIN, srcPort, dstPort, srcIP, dstIP) .withHeaderMap(reqAccessor.retrieveHeaderMap()) .withDate(reqAccessor.getHeader(DATE_HEADER)) .withHost(reqAccessor.getHeader(HOST_HEADER)) @@ -113,38 +119,33 @@ public static void service(@AllArguments Object[] args, .withMethod(reqAccessor.getMethod()) .withRequest(request) .withURL(reqAccessor.getRequestUrl()); - EventBus.publish(requestEvent); - } catch (Throwable e) { - log.error("DiSCo(Web) Failed to retrieve request data from servlet service."); - } - } + EventBus.publish(requestEvent); + } catch (Throwable e) { + log.error("DiSCo(Web) Failed to retrieve request data from servlet service."); + } // call the original, catching anything it throws - try { + try { zuper.call(); - } catch (Throwable t) { + } catch (Throwable t) { throwable = t; - } + } - if (txStackDepth == 0) { - try { - Object response = args[1]; - HttpServletResponseAccessor respAccessor = (HttpServletResponseAccessor)response; + try { + Object response = args[1]; + HttpServletResponseAccessor respAccessor = (HttpServletResponseAccessor)response; - int statusCode = respAccessor.getStatus(); - responseEvent = new HttpServletNetworkResponseEvent(EVENT_ORIGIN, requestEvent) + int statusCode = respAccessor.getStatus(); + responseEvent = new HttpServletNetworkResponseEvent(EVENT_ORIGIN, requestEvent) .withHeaderMap(respAccessor.retrieveHeaderMap()) .withStatusCode(statusCode) .withResponse(response); - EventBus.publish(responseEvent); - } catch (Throwable t) { + EventBus.publish(responseEvent); + } catch (Throwable t) { log.error("DiSCo(Web) Failed to retrieve response data from service."); - } - } - + } //match the create() call with a destroy() in all cases TransactionContext.destroy(); - //rethrow anything if (throwable != null) { throw throwable; From a2190a17a7179862ae79faaa97fa041f714bc37e Mon Sep 17 00:00:00 2001 From: Sai Siripurapu Date: Tue, 16 Jun 2020 10:40:09 -0700 Subject: [PATCH 16/45] Modelling Service down stream cancellation events --- .../AbstractServiceCancellationEvent.java | 61 +++++++++++++++++++ .../disco/agent/event/CancellationEvent.java | 23 +++++++ .../agent/event/ServiceCancellationEvent.java | 26 ++++++++ .../ServiceDownstreamCancellationEvent.java | 39 ++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/AbstractServiceCancellationEvent.java create mode 100644 disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/CancellationEvent.java create mode 100644 disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/ServiceCancellationEvent.java create mode 100644 disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/ServiceDownstreamCancellationEvent.java diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/AbstractServiceCancellationEvent.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/AbstractServiceCancellationEvent.java new file mode 100644 index 0000000..50328db --- /dev/null +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/AbstractServiceCancellationEvent.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.disco.agent.event; + +/** + * An event issued to the event bus when service request is cancelled + */ +public abstract class AbstractServiceCancellationEvent extends AbstractServiceEvent implements ServiceCancellationEvent { + /** + * Constructor for a AbstractServiceCancellationEvent + * + * @param origin the origin of this event e.g. 'Web' or 'gRPC' + * @param service the service name e.g. WeatherService + * @param operation the operation name e.g getWeather + * @param requestEvent the associated cancelled request Event + */ + public AbstractServiceCancellationEvent(String origin, String service, String operation, ServiceRequestEvent requestEvent) { + super(origin, service, operation); + withRequest(requestEvent); + } + + /** + * Data keys + */ + enum DataKey { + /** + * The complete request object to the service + */ + REQUEST + } + + /** + * Add a request object to this event + * @param request the downstream service request object + * @return the 'this' for method chaining + */ + public AbstractServiceCancellationEvent withRequest(ServiceRequestEvent request) { + withData(DataKey.REQUEST.name(), request); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public ServiceRequestEvent getRequest() { + return ServiceRequestEvent.class.cast(getData(DataKey.REQUEST.name())); + } +} diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/CancellationEvent.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/CancellationEvent.java new file mode 100644 index 0000000..43bdd9b --- /dev/null +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/CancellationEvent.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.disco.agent.event; + + +/** + * An event issued to the event bus when request is cancelled + */ +public interface CancellationEvent extends Event { +//Marker interface to capture the categorization of events +} diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/ServiceCancellationEvent.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/ServiceCancellationEvent.java new file mode 100644 index 0000000..32d9961 --- /dev/null +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/ServiceCancellationEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.disco.agent.event; +/** + * Specialization of ServiceEvent for service requests. Requests and Cancellations are dispatched separately, + * Events are triggered before and after cancellation of request + */ +public interface ServiceCancellationEvent extends CancellationEvent{ + /** + * Get the associated request event + * @return the service request event which is cancelled {@link ServiceRequestEvent} + */ + ServiceRequestEvent getRequest(); +} diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/ServiceDownstreamCancellationEvent.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/ServiceDownstreamCancellationEvent.java new file mode 100644 index 0000000..f57208e --- /dev/null +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/ServiceDownstreamCancellationEvent.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.disco.agent.event; +/** + * An event issued to the event bus when downstream service request is cancelled + */ +public class ServiceDownstreamCancellationEvent extends AbstractServiceCancellationEvent{ + /** + * Constructor for a ServiceDownstreamCancellationEvent + * + * @param origin the origin of this event e.g. 'Web' or 'gRPC' + * @param service the service name e.g. WeatherService + * @param operation the operation name e.g getWeather + * @param requestEvent the associated cancelled request Event + */ + public ServiceDownstreamCancellationEvent(String origin, String service, String operation, ServiceDownstreamRequestEvent requestEvent) { + super(origin, service, operation, requestEvent); + } + + /** + * {@inheritDoc} + */ + @Override + public ServiceEvent.Type getType() { + return ServiceEvent.Type.DOWNSTREAM; + } +} From c3a79aa5ad086d5de959d4d2a25bf928908d5374 Mon Sep 17 00:00:00 2001 From: Connell Date: Tue, 23 Jun 2020 10:57:35 -0700 Subject: [PATCH 17/45] Make isWithinCreatedContext() null safe for clients --- .../disco/agent/reflect/concurrent/TransactionContext.java | 5 +++-- .../agent/reflect/concurrent/TransactionContextTests.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java index e423d6a..d691822 100644 --- a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java @@ -200,11 +200,12 @@ public static String getUninitializedTransactionContextValue() { * around every activity more reliable, instead of Support package authors having to remember to do it * @return true if we think we're currently inside a created Transaction Context, else false */ - public static Boolean isWithinCreatedContext() { - return ReflectiveCall.returning(Boolean.class) + public static boolean isWithinCreatedContext() { + Boolean result = ReflectiveCall.returning(Boolean.class) .ofClass(TRANSACTIONCONTEXT_CLASS) .ofMethod("isWithinCreatedContext") .call(); + return result == null ? false : result; } /** diff --git a/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/concurrent/TransactionContextTests.java b/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/concurrent/TransactionContextTests.java index f7f67b1..c5e4df1 100644 --- a/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/concurrent/TransactionContextTests.java +++ b/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/reflect/concurrent/TransactionContextTests.java @@ -139,6 +139,6 @@ public void testNoIllegalArgumentExceptionWhenGetMetadataCalledWithLegalIdentifi @Test public void testIsWithinCreatedContextWhenAgentNotLoaded() { - TransactionContext.isWithinCreatedContext(); + Assert.assertFalse(TransactionContext.isWithinCreatedContext()); } } From bf13b1c06d19e9bedff46485531898da0da5dfb9 Mon Sep 17 00:00:00 2001 From: Sai Siripurapu Date: Wed, 24 Jun 2020 15:33:57 -0700 Subject: [PATCH 18/45] Refactoring Apache Http service downstream request and response event creation in factory class --- .../web/apache/event/ApacheEventFactory.java | 50 ++++++++++++- .../ApacheHttpClientInterceptor.java | 28 +------- .../apache/event/ApacheEventFactoryTests.java | 71 ++++++++++++++++++ .../ApacheHttpClientInterceptorTests.java | 72 +++---------------- .../apache/source/ApacheClientTestUtil.java | 23 ++++++ .../apache/source/ApacheTestConstants.java | 9 +++ .../source/InterceptedBasicHttpRequest.java | 30 ++++++++ .../source/InterceptedBasicHttpResponse.java | 22 ++++++ .../source/InterceptedHttpRequestBase.java | 29 ++++++++ 9 files changed, 242 insertions(+), 92 deletions(-) create mode 100644 disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java create mode 100644 disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/ApacheClientTestUtil.java create mode 100644 disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/ApacheTestConstants.java create mode 100644 disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedBasicHttpRequest.java create mode 100644 disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedBasicHttpResponse.java create mode 100644 disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedHttpRequestBase.java diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactory.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactory.java index 6a0b9b1..afbdb4f 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactory.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactory.java @@ -15,13 +15,59 @@ package software.amazon.disco.agent.web.apache.event; +import software.amazon.disco.agent.event.HttpServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.HttpServiceDownstreamResponseEvent; +import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; +import software.amazon.disco.agent.web.apache.utils.HttpRequestBaseAccessor; +import software.amazon.disco.agent.web.apache.utils.HttpResponseAccessor; /** * Create our private events, so that listeners do not have public access to them */ public class ApacheEventFactory { - public static ApacheHttpServiceDownstreamRequestEvent createDownstreamRequestEvent(String origin, String service, String operation, HttpRequestAccessor accessor) { - return new ApacheHttpServiceDownstreamRequestEvent(origin, service, operation, accessor); + /** + * Create our private events, so that listeners do not have public access to them + * @param origin the origin of the downstream call e.g. 'Web' + * @param accessor a HttpRequestAccessor to get uri and HTTP method + * @return a {@link ApacheHttpServiceDownstreamRequestEvent} + */ + public static HttpServiceDownstreamRequestEvent createDownstreamRequestEvent(String origin, HttpRequestAccessor accessor) { + String uri; + String method; + if (accessor instanceof HttpRequestBaseAccessor) { + //we can retrieve the data in a streamlined way, avoiding internal production of the RequestLine + HttpRequestBaseAccessor baseAccessor = (HttpRequestBaseAccessor)accessor; + uri = baseAccessor.getUri(); + method = baseAccessor.getMethod(); + } else { + uri = accessor.getUriFromRequestLine(); + method = accessor.getMethodFromRequestLine(); + } + //TODO - using uri and method as service and operation name is unsatisfactory. + ApacheHttpServiceDownstreamRequestEvent requestEvent = new ApacheHttpServiceDownstreamRequestEvent(origin, uri, method, accessor);; + requestEvent.withMethod(method); + requestEvent.withUri(uri); + return requestEvent; + } + + /** + * Create response event with HttpResponse for apache client downstream call + * @param responseAccessor a HttpResponseAccessor to get status code etc. + * @param requestEvent Previously published ServiceDownstreamRequestEvent + * @param throwable The throwable if the request fails + * @return a {@link HttpServiceDownstreamResponseEvent}. + */ + public static ServiceDownstreamResponseEvent createServiceResponseEvent(final HttpResponseAccessor responseAccessor, final ServiceDownstreamRequestEvent requestEvent, final Throwable throwable) { + HttpServiceDownstreamResponseEvent responseEvent = new HttpServiceDownstreamResponseEvent(requestEvent.getOrigin(), requestEvent.getService(), requestEvent.getOperation(), requestEvent); + if (throwable != null) { + responseEvent.withThrown(throwable); + } + if (responseAccessor != null) { + responseEvent.withStatusCode(responseAccessor.getStatusCode()); + responseEvent.withContentLength(responseAccessor.getContentLength()); + } + return responseEvent; } } diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java index bd1193d..3b9536d 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java @@ -16,10 +16,8 @@ package software.amazon.disco.agent.web.apache.httpclient; import software.amazon.disco.agent.event.HttpServiceDownstreamRequestEvent; -import software.amazon.disco.agent.event.HttpServiceDownstreamResponseEvent; import software.amazon.disco.agent.web.apache.event.ApacheEventFactory; import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; -import software.amazon.disco.agent.web.apache.utils.HttpRequestBaseAccessor; import software.amazon.disco.agent.web.apache.utils.HttpResponseAccessor; import software.amazon.disco.agent.web.apache.utils.MethodInterceptionCounter; import software.amazon.disco.agent.event.EventBus; @@ -53,7 +51,6 @@ public class ApacheHttpClientInterceptor implements Installable { private static final Logger log = LogManager.getLogger(ApacheHttpClientInterceptor.class); private static final MethodInterceptionCounter METHOD_INTERCEPTION_COUNTER = new MethodInterceptionCounter(); - static final String APACHE_HTTP_CLIENT_ORIGIN = "ApacheHttpClient"; /** @@ -142,21 +139,7 @@ private static Object call(final Callable zuper) throws Throwable { * @return The published ServiceDownstreamRequestEvent, which is needed when publishing ServiceDownstreamResponseEvent later */ private static HttpServiceDownstreamRequestEvent publishRequestEvent(final HttpRequestAccessor requestAccessor) { - String uri; - String method; - if (requestAccessor instanceof HttpRequestBaseAccessor) { - //we can retrieve the data in a streamlined way, avoiding internal production of the RequestLine - HttpRequestBaseAccessor baseAccessor = (HttpRequestBaseAccessor)requestAccessor; - uri = baseAccessor.getUri(); - method = baseAccessor.getMethod(); - } else { - uri = requestAccessor.getUriFromRequestLine(); - method = requestAccessor.getMethodFromRequestLine(); - } - //TODO - using uri and method as service and operation name is unsatisfactory. - HttpServiceDownstreamRequestEvent requestEvent = ApacheEventFactory.createDownstreamRequestEvent(APACHE_HTTP_CLIENT_ORIGIN, uri, method, requestAccessor); - requestEvent.withMethod(method); - requestEvent.withUri(uri); + HttpServiceDownstreamRequestEvent requestEvent = ApacheEventFactory.createDownstreamRequestEvent(APACHE_HTTP_CLIENT_ORIGIN, requestAccessor); EventBus.publish(requestEvent); return requestEvent; } @@ -168,14 +151,7 @@ private static HttpServiceDownstreamRequestEvent publishRequestEvent(final HttpR * @param throwable The throwable if the request fails */ private static void publishResponseEvent(final HttpResponseAccessor responseAccessor, final ServiceDownstreamRequestEvent requestEvent, final Throwable throwable) { - HttpServiceDownstreamResponseEvent responseEvent = new HttpServiceDownstreamResponseEvent(APACHE_HTTP_CLIENT_ORIGIN, requestEvent.getService(), requestEvent.getOperation(), requestEvent); - if(throwable != null) { - responseEvent.withThrown(throwable); - } - if (responseAccessor != null) { - responseEvent.withStatusCode(responseAccessor.getStatusCode()); - responseEvent.withContentLength(responseAccessor.getContentLength()); - } + ServiceDownstreamResponseEvent responseEvent = ApacheEventFactory.createServiceResponseEvent(responseAccessor, requestEvent, throwable); EventBus.publish(responseEvent); } diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java new file mode 100644 index 0000000..c9f128d --- /dev/null +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java @@ -0,0 +1,71 @@ +package software.amazon.disco.agent.web.apache.event; + +import org.apache.http.ProtocolVersion; +import org.junit.Before; +import org.junit.Test; +import software.amazon.disco.agent.concurrent.TransactionContext; +import software.amazon.disco.agent.event.EventBus; +import software.amazon.disco.agent.event.HttpServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; +import software.amazon.disco.agent.web.apache.source.MockEventBusListener; +import software.amazon.disco.agent.web.apache.source.ApacheClientTestUtil; +import software.amazon.disco.agent.web.apache.source.ApacheTestConstants; +import software.amazon.disco.agent.web.apache.utils.HttpResponseAccessor; +import software.amazon.disco.agent.web.apache.source.InterceptedBasicHttpRequest; +import software.amazon.disco.agent.web.apache.source.InterceptedBasicHttpResponse; +import software.amazon.disco.agent.web.apache.source.InterceptedHttpRequestBase; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; + +public class ApacheEventFactoryTests { + + private InterceptedBasicHttpRequest accessor; + private InterceptedHttpRequestBase httpRequestBaseAccessor; + private MockEventBusListener mockEventBusListener; + + @Before + public void before() { + accessor = new InterceptedBasicHttpRequest(); + mockEventBusListener = new MockEventBusListener(); + httpRequestBaseAccessor = new InterceptedHttpRequestBase(); + TransactionContext.create(); + EventBus.addListener(mockEventBusListener); + } + @Test + public void testForRequestEventCreationForRequest() { + HttpServiceDownstreamRequestEvent event = ApacheEventFactory.createDownstreamRequestEvent(ApacheTestConstants.APACHE_HTTP_CLIENT_ORIGIN, + accessor); + ApacheClientTestUtil.verifyServiceRequestEvent(event); + assertFalse(accessor.getHeaders().containsKey("TEST")); + event.replaceHeader("TEST","TEST"); + assertTrue(accessor.getHeaders().containsKey("TEST")); + } + + @Test + public void testForRequestEventCreationForRequestBase() { + HttpServiceDownstreamRequestEvent event = ApacheEventFactory.createDownstreamRequestEvent(ApacheTestConstants.APACHE_HTTP_CLIENT_ORIGIN, + httpRequestBaseAccessor); + ApacheClientTestUtil.verifyServiceRequestEvent(event); + } + + @Test + public void testForResponseEventCreationForSuccessfulResponse() { + HttpResponseAccessor expectedResponse = new InterceptedBasicHttpResponse(new ProtocolVersion("protocol", 1, 1), 200, ""); + HttpServiceDownstreamRequestEvent event = ApacheEventFactory.createDownstreamRequestEvent(ApacheTestConstants.APACHE_HTTP_CLIENT_ORIGIN,accessor); + ServiceDownstreamResponseEvent responseEvent = ApacheEventFactory.createServiceResponseEvent(expectedResponse,event,null); + ApacheClientTestUtil.verifyServiceResponseEvent(responseEvent); + assertNull(responseEvent.getThrown()); + } + + @Test + public void testForResponseEventCreationForFailureResponse() { + Exception error = new Exception("CUSTOM EXCEPTION"); + HttpServiceDownstreamRequestEvent event = ApacheEventFactory.createDownstreamRequestEvent(ApacheTestConstants.APACHE_HTTP_CLIENT_ORIGIN,accessor); + ServiceDownstreamResponseEvent responseEvent = ApacheEventFactory.createServiceResponseEvent(null,event,error); + ApacheClientTestUtil.verifyServiceResponseEvent(responseEvent); + assertEquals(responseEvent.getThrown(),error); + } +} diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java index 2080aba..2d30287 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java @@ -15,11 +15,8 @@ package software.amazon.disco.agent.web.apache.httpclient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; -import org.apache.http.message.BasicHttpRequest; import software.amazon.disco.agent.concurrent.TransactionContext; import software.amazon.disco.agent.event.Event; import software.amazon.disco.agent.event.EventBus; @@ -34,22 +31,21 @@ import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.ProtocolVersion; -import org.apache.http.RequestLine; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.message.BasicHttpResponse; import org.apache.http.params.HttpParams; import org.apache.http.protocol.HttpContext; import org.junit.After; import org.junit.Before; import org.junit.Test; + import software.amazon.disco.agent.web.apache.source.MockEventBusListener; -import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; -import software.amazon.disco.agent.web.apache.utils.HttpRequestBaseAccessor; -import software.amazon.disco.agent.web.apache.utils.HttpResponseAccessor; +import software.amazon.disco.agent.web.apache.source.ApacheClientTestUtil; +import software.amazon.disco.agent.web.apache.source.InterceptedBasicHttpResponse; +import software.amazon.disco.agent.web.apache.source.InterceptedHttpRequestBase; import java.io.IOException; import java.lang.reflect.Method; @@ -66,9 +62,6 @@ public class ApacheHttpClientInterceptorTests { - private static final String URI = "http://amazon.com/explore/something"; - private static final String METHOD = "GET"; - private ApacheHttpClientInterceptor interceptor; private HttpResponse expectedResponse; private IOException expectedIOException; @@ -191,11 +184,11 @@ public void testInterceptorSucceededOnChainedMethods() throws Throwable { assertEquals(2, events.size()); // Verify the Request Event - verifyServiceRequestEvent((HttpServiceDownstreamRequestEvent) events.get(0)); + ApacheClientTestUtil.verifyServiceRequestEvent((HttpServiceDownstreamRequestEvent) events.get(0)); // Verify the Response Event HttpServiceDownstreamResponseEvent serviceDownstreamResponseEvent = (HttpServiceDownstreamResponseEvent) events.get(1); - verifyServiceResponseEvent(serviceDownstreamResponseEvent); + ApacheClientTestUtil.verifyServiceResponseEvent(serviceDownstreamResponseEvent); assertNull(serviceDownstreamResponseEvent.getResponse()); assertNull(serviceDownstreamResponseEvent.getThrown()); assertEquals(200, serviceDownstreamResponseEvent.getStatusCode()); @@ -223,11 +216,11 @@ public void testInterceptorSucceededAndReThrowOnException() throws Throwable { assertEquals(2, events.size()); // Verify the Request Event - verifyServiceRequestEvent((HttpServiceDownstreamRequestEvent) events.get(0)); + ApacheClientTestUtil.verifyServiceRequestEvent((HttpServiceDownstreamRequestEvent) events.get(0)); // Verify the Response Event ServiceDownstreamResponseEvent serviceDownstreamResponseEvent = (ServiceDownstreamResponseEvent) events.get(1); - verifyServiceResponseEvent(serviceDownstreamResponseEvent); + ApacheClientTestUtil.verifyServiceResponseEvent(serviceDownstreamResponseEvent); assertNull(serviceDownstreamResponseEvent.getResponse()); assertEquals(expectedIOException, serviceDownstreamResponseEvent.getThrown()); @@ -235,18 +228,6 @@ public void testInterceptorSucceededAndReThrowOnException() throws Throwable { } } - private static void verifyServiceRequestEvent(final HttpServiceDownstreamRequestEvent serviceDownstreamRequestEvent) { - assertEquals(METHOD, serviceDownstreamRequestEvent.getMethod()); - assertEquals(URI, serviceDownstreamRequestEvent.getUri()); - assertEquals(ApacheHttpClientInterceptor.APACHE_HTTP_CLIENT_ORIGIN, serviceDownstreamRequestEvent.getOrigin()); - assertNull(serviceDownstreamRequestEvent.getRequest()); - } - - private static void verifyServiceResponseEvent(final ServiceDownstreamResponseEvent serviceDownstreamResponseEvent) { - assertEquals(METHOD, serviceDownstreamResponseEvent.getOperation()); - assertEquals(URI, serviceDownstreamResponseEvent.getService()); - assertEquals(ApacheHttpClientInterceptor.APACHE_HTTP_CLIENT_ORIGIN, serviceDownstreamResponseEvent.getOrigin()); - } /** * Helper method to test the class matcher matching @@ -417,48 +398,11 @@ public HttpResponse execute(WillThrowExceptionOnExecutionHttpRequest request) th abstract class SomeClassSuperTypeIsHttpRequest implements HttpRequest { } } - public class InterceptedHttpRequestBase extends HttpRequestBase implements HttpRequestBaseAccessor, HttpRequestAccessor { - - @Override - public String getMethod() { - return METHOD; - } - - @Override - public String getUri() { - return URI; - } - - @Override - public String getMethodFromRequestLine() { - return null; - } - - @Override - public String getUriFromRequestLine() { - return null; - } - } - public class WillThrowExceptionOnExecutionHttpRequest extends InterceptedHttpRequestBase { } /** * A subclass of BasicHttpResponse which pretends that interception occurred, and hence also implements the Accessor */ - public class InterceptedBasicHttpResponse extends BasicHttpResponse implements HttpResponseAccessor { - public InterceptedBasicHttpResponse(final ProtocolVersion ver, final int code, final String reason) { - super(ver, code, reason); - } - - @Override - public int getStatusCode() { - return super.getStatusLine().getStatusCode(); - } - @Override - public long getContentLength() { - return 0; - } - } } \ No newline at end of file diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/ApacheClientTestUtil.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/ApacheClientTestUtil.java new file mode 100644 index 0000000..22874e0 --- /dev/null +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/ApacheClientTestUtil.java @@ -0,0 +1,23 @@ +package software.amazon.disco.agent.web.apache.source; + +import software.amazon.disco.agent.event.HttpServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ApacheClientTestUtil { + + public static void verifyServiceRequestEvent(final HttpServiceDownstreamRequestEvent serviceDownstreamRequestEvent) { + assertEquals(ApacheTestConstants.METHOD, serviceDownstreamRequestEvent.getMethod()); + assertEquals(ApacheTestConstants.URI, serviceDownstreamRequestEvent.getUri()); + assertEquals(ApacheTestConstants.APACHE_HTTP_CLIENT_ORIGIN, serviceDownstreamRequestEvent.getOrigin()); + assertNull(serviceDownstreamRequestEvent.getRequest()); + } + + public static void verifyServiceResponseEvent(final ServiceDownstreamResponseEvent serviceDownstreamResponseEvent) { + assertEquals(ApacheTestConstants.METHOD, serviceDownstreamResponseEvent.getOperation()); + assertEquals(ApacheTestConstants.URI, serviceDownstreamResponseEvent.getService()); + assertEquals(ApacheTestConstants.APACHE_HTTP_CLIENT_ORIGIN, serviceDownstreamResponseEvent.getOrigin()); + } +} diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/ApacheTestConstants.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/ApacheTestConstants.java new file mode 100644 index 0000000..3cb09fe --- /dev/null +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/ApacheTestConstants.java @@ -0,0 +1,9 @@ +package software.amazon.disco.agent.web.apache.source; + +public class ApacheTestConstants { + + public static final String URI = "http://amazon.com/explore/something"; + public static final String METHOD = "GET"; + public static final String APACHE_HTTP_CLIENT_ORIGIN = "ApacheHttpClient"; + +} diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedBasicHttpRequest.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedBasicHttpRequest.java new file mode 100644 index 0000000..7bfbd85 --- /dev/null +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedBasicHttpRequest.java @@ -0,0 +1,30 @@ +package software.amazon.disco.agent.web.apache.source; + +import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; + +import java.util.HashMap; + +public class InterceptedBasicHttpRequest implements HttpRequestAccessor { + + private HashMap headers = new HashMap(); + @Override + public String getMethodFromRequestLine() { + return ApacheTestConstants.METHOD; + } + @Override + public String getUriFromRequestLine() { + return ApacheTestConstants.URI; + } + @Override + public void addHeader(String name, String value) { + headers.put(name,value); + } + @Override + public void removeHeaders(String name) { + headers.remove(name); + } + public HashMap getHeaders() { + return headers; + } + +} diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedBasicHttpResponse.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedBasicHttpResponse.java new file mode 100644 index 0000000..3650fd0 --- /dev/null +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedBasicHttpResponse.java @@ -0,0 +1,22 @@ +package software.amazon.disco.agent.web.apache.source; + +import org.apache.http.ProtocolVersion; +import org.apache.http.message.BasicHttpResponse; +import software.amazon.disco.agent.web.apache.utils.HttpResponseAccessor; + +public class InterceptedBasicHttpResponse extends BasicHttpResponse implements HttpResponseAccessor { + + public InterceptedBasicHttpResponse(final ProtocolVersion ver, final int code, final String reason) { + super(ver, code, reason); + } + @Override + public int getStatusCode() { + return super.getStatusLine().getStatusCode(); + } + + @Override + public long getContentLength() { + return 0; + } + + } \ No newline at end of file diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedHttpRequestBase.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedHttpRequestBase.java new file mode 100644 index 0000000..b85429d --- /dev/null +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/source/InterceptedHttpRequestBase.java @@ -0,0 +1,29 @@ +package software.amazon.disco.agent.web.apache.source; + +import org.apache.http.client.methods.HttpRequestBase; +import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; +import software.amazon.disco.agent.web.apache.utils.HttpRequestBaseAccessor; + +public class InterceptedHttpRequestBase extends HttpRequestBase implements HttpRequestBaseAccessor, HttpRequestAccessor { + + @Override + public String getMethod() { + return ApacheTestConstants.METHOD; + } + + @Override + public String getUri() { + return ApacheTestConstants.URI; + } + + @Override + public String getMethodFromRequestLine() { + return null; + } + + @Override + public String getUriFromRequestLine() { + return null; + } + + } From cbbe6518d1ac2743e03ab57ff537d91dcdc87c54 Mon Sep 17 00:00:00 2001 From: William Armiros Date: Thu, 25 Jun 2020 13:50:54 -0700 Subject: [PATCH 19/45] fixed NPE in httpResponseEvent --- .../HttpServiceDownstreamResponseEvent.java | 10 ++++---- .../disco/agent/event/ServiceEventTests.java | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HttpServiceDownstreamResponseEvent.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HttpServiceDownstreamResponseEvent.java index 01fbf5c..960f0fa 100644 --- a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HttpServiceDownstreamResponseEvent.java +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HttpServiceDownstreamResponseEvent.java @@ -67,17 +67,19 @@ public HttpServiceDownstreamResponseEvent withContentLength(long contentLength) /** * Get the status code stored in the Event - * @return the HTTP status code + * @return the HTTP status code, or -1 if not available */ public int getStatusCode() { - return (int)getData(DataKey.STATUS_CODE.name()); + Object statusCode = getData(DataKey.STATUS_CODE.name()); + return statusCode == null ? -1 : (int)statusCode; } /** * Get the content length stored in the Event - * @return the HTTP content length + * @return the HTTP content length, or -1 if not available */ public long getContentLength() { - return (long)getData(DataKey.CONTENT_LENGTH.name()); + Object contentLength = getData(DataKey.CONTENT_LENGTH.name()); + return contentLength == null ? -1L : (long)contentLength; } } diff --git a/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/event/ServiceEventTests.java b/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/event/ServiceEventTests.java index c9943da..5764b14 100644 --- a/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/event/ServiceEventTests.java +++ b/disco-java-agent/disco-java-agent-api/src/test/java/software/amazon/disco/agent/event/ServiceEventTests.java @@ -66,6 +66,30 @@ public void testServiceDownstreamResponseEvent() { Assert.assertEquals(thrown, event.getThrown()); } + @Test + public void testHttpServiceDownstreamResponseEvent() { + ServiceDownstreamRequestEvent requestEvent = Mockito.mock(ServiceDownstreamRequestEvent.class); + HttpServiceDownstreamResponseEvent responseEvent = new HttpServiceDownstreamResponseEvent("Origin", "Service", "Operation", requestEvent) + .withStatusCode(200) + .withContentLength(42L); + + test(responseEvent); + + Assert.assertEquals(200, responseEvent.getStatusCode()); + Assert.assertEquals(42L, responseEvent.getContentLength()); + } + + @Test + public void testHttpServiceDownstreamResponseEventWithoutResponse() { + ServiceDownstreamRequestEvent requestEvent = Mockito.mock(ServiceDownstreamRequestEvent.class); + HttpServiceDownstreamResponseEvent responseEvent = new HttpServiceDownstreamResponseEvent("Origin", "Service", "Operation", requestEvent); + + test(responseEvent); + + Assert.assertEquals(-1, responseEvent.getStatusCode()); + Assert.assertEquals(-1L, responseEvent.getContentLength()); + } + private void test(AbstractServiceEvent event) { Assert.assertEquals("Origin", event.getOrigin()); Assert.assertEquals("Service", event.getService()); From 700042e193263db69aa223e85bdb60655eec213b Mon Sep 17 00:00:00 2001 From: Connell Date: Fri, 26 Jun 2020 11:48:04 -0700 Subject: [PATCH 20/45] Fix mishandling of Apache client execute() methods which take a ResponseHandler instead of returning HttpResponse --- .../ApacheHttpClientInterceptorTests.java | 43 +++++++++++++++++++ .../ApacheHttpClientInterceptor.java | 6 ++- .../ApacheHttpClientInterceptorTests.java | 24 +++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/apache/httpclient/ApacheHttpClientInterceptorTests.java b/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/apache/httpclient/ApacheHttpClientInterceptorTests.java index aa99d57..ef6bd1f 100644 --- a/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/apache/httpclient/ApacheHttpClientInterceptorTests.java +++ b/disco-java-agent-web/disco-java-agent-web-plugin/src/test/java/software/amazon/disco/agent/integtest/web/apache/httpclient/ApacheHttpClientInterceptorTests.java @@ -18,6 +18,7 @@ import org.apache.http.HttpHost; import org.apache.http.ProtocolVersion; import org.apache.http.RequestLine; +import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.StringEntity; @@ -25,6 +26,7 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicHttpRequest; import org.apache.http.message.BasicRequestLine; +import org.mockito.Mockito; import software.amazon.disco.agent.event.HttpServiceDownstreamRequestEvent; import software.amazon.disco.agent.event.HttpServiceDownstreamResponseEvent; import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent; @@ -84,6 +86,27 @@ public void testMinimalClient() throws Exception { assertEquals(1, testListener.responseEvents.size()); } + @Test + public void testMinimalClientWithResponseHandler() throws Exception { + ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class); + try (CloseableHttpClient httpClient = HttpClients.createMinimal()) { + HttpGet request = new HttpGet("https://amazon.com"); + + try { + Mockito.when(responseHandler.handleResponse(Mockito.any())).thenReturn(new Object()); + httpClient.execute(request, responseHandler); + } catch (IOException e) { + //swallow + } catch (Exception e) { + Assert.fail(); + } + } + + Mockito.verify(responseHandler).handleResponse(Mockito.any()); + assertEquals(1, testListener.requestEvents.size()); + assertEquals(1, testListener.responseEvents.size()); + } + @Test public void testDefaultClient() throws Exception { try (CloseableHttpClient httpClient = HttpClients.createDefault()) { @@ -99,6 +122,26 @@ public void testDefaultClient() throws Exception { assertEquals(1, testListener.responseEvents.size()); } + @Test + public void testDefaultClientWithResponseHandler() throws Exception { + ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class); + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet request = new HttpGet("https://amazon.com"); + try { + Mockito.when(responseHandler.handleResponse(Mockito.any())).thenReturn(new Object()); + httpClient.execute(request, responseHandler); + } catch (IOException e) { + //swallow + } catch (Exception e) { + Assert.fail(); + } + } + + Mockito.verify(responseHandler).handleResponse(Mockito.any()); + assertEquals(1, testListener.requestEvents.size()); + assertEquals(1, testListener.responseEvents.size()); + } + @Test public void testDefaultClientWithBasicHttpRequest() throws Exception { try (CloseableHttpClient httpClient = HttpClients.createDefault()) { diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java index 3b9536d..691057e 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java @@ -90,7 +90,11 @@ public static Object intercept(@AllArguments final Object[] args, throwable = t; } finally { // publish response event - HttpResponseAccessor responseAccessor = (HttpResponseAccessor)response; + HttpResponseAccessor responseAccessor = null; + //we currently do not support the flavors of execute() which take a ResponseHandler and return the T. + if (response instanceof HttpResponseAccessor) { + responseAccessor = (HttpResponseAccessor)response; + } publishResponseEvent(responseAccessor, requestEvent, throwable); if (throwable != null) { throw throwable; diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java index 2d30287..ff04d0b 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java @@ -17,6 +17,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.mockito.Mockito; import software.amazon.disco.agent.concurrent.TransactionContext; import software.amazon.disco.agent.event.Event; import software.amazon.disco.agent.event.EventBus; @@ -228,6 +229,29 @@ public void testInterceptorSucceededAndReThrowOnException() throws Throwable { } } + @Test + public void testInterceptorSucceedsOnResponseHandlerExecute() throws Throwable { + HttpUriRequest get = new InterceptedHttpRequestBase(); + + SomeChainedExecuteMethodsHttpClient someHttpClient = new SomeChainedExecuteMethodsHttpClient(); + ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class); + Mockito.when(responseHandler.handleResponse(Mockito.any())).thenReturn(new Object()); + ApacheHttpClientInterceptor.intercept(new Object[] {get}, "origin", () -> someHttpClient.execute(get, responseHandler)); + + List events = mockEventBusListener.getReceivedEvents(); + + // Verify only one of interceptions does the interceptor business logic even if there is a method chaining, + // as a result, only two service downstream events are published (request/response) + assertEquals(2, events.size()); + + // Verify the Request Event + ApacheClientTestUtil.verifyServiceRequestEvent((HttpServiceDownstreamRequestEvent) events.get(0)); + + // Verify the Response Event + ServiceDownstreamResponseEvent serviceDownstreamResponseEvent = (ServiceDownstreamResponseEvent) events.get(1); + ApacheClientTestUtil.verifyServiceResponseEvent(serviceDownstreamResponseEvent); + } + /** * Helper method to test the class matcher matching From e9f673179849235503f6f1dd218c3705199b496c Mon Sep 17 00:00:00 2001 From: Connell Date: Mon, 6 Jul 2020 09:15:59 -0700 Subject: [PATCH 21/45] Fix regex to produce list of installables, without including all the separators and spaces --- .../amazon/disco/agent/plugin/PluginDiscovery.java | 13 +++++++++++-- .../disco/agent/plugin/PluginDiscoveryTests.java | 8 ++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java index ff7a507..ebdf71f 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java @@ -300,7 +300,7 @@ static void processInitClass(String pluginName, String initClassName, boolean bo */ static void processInstallableClasses(String pluginName, String installableClassNames, boolean bootstrap) throws Exception { if (installableClassNames != null) { - String[] classNames = installableClassNames.trim().split("\\s"); + String[] classNames = splitString(installableClassNames); for (String className: classNames) { try { Class clazz = classForName(className.trim(), bootstrap); @@ -326,7 +326,7 @@ static void processInstallableClasses(String pluginName, String installableClass */ static void processListenerClasses(String pluginName, String listenerClassNames, boolean bootstrap) throws Exception { if (listenerClassNames != null) { - String[] classNames = listenerClassNames.trim().split("\\s"); + String[] classNames = splitString(listenerClassNames); for (String className : classNames) { if (LogManager.isDebugEnabled()) { log.debug("DiSCo(Core) attempting to add Listener from plugin using class: " + className); @@ -360,4 +360,13 @@ static Class classForName(String name, boolean bootstrap) throws Exception { : Class.forName(name, false, ClassLoader.getSystemClassLoader()); } + /** + * Helper method to take a list of items e.g. Disco-Installable-Classes and produce a Collection of the individual entries + * @param input the String from the manifest e.g. " com.foo.Foo com.foo.Bar " + * @return the split results e.g. ["com.foo.Foo", "com.foo.Bar"] + */ + static String[] splitString(String input) { + return input.trim().split("\\s+"); + } + } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java index eaffa87..36ff4da 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java @@ -170,6 +170,14 @@ public void testManifestWithoutDiscoAttributesSafelySkipped() throws Exception { Assert.assertTrue(outcomes.isEmpty()); } + @Test + public void testSplitString() { + String[] result = PluginDiscovery.splitString(" com.foo.Foo com.foo.Bar "); + Assert.assertEquals(2, result.length); + Assert.assertEquals("com.foo.Foo", result[0]); + Assert.assertEquals("com.foo.Bar", result[1]); + } + private Collection scanAndApply(Instrumentation instrumentation, AgentConfig agentConfig) { PluginDiscovery.scan(instrumentation, agentConfig); installables.addAll(PluginDiscovery.processInstallables()); From 78ec4827bfceb7af88731f39684aa3ed2ad2cb23 Mon Sep 17 00:00:00 2001 From: Smith Date: Fri, 10 Jul 2020 09:36:16 -0700 Subject: [PATCH 22/45] removing unecessary log lines which would also introduce string concatenation overhead for services --- .../amazon/disco/agent/reflect/ReflectiveCall.java | 14 +------------- .../reflect/concurrent/TransactionContext.java | 6 ------ .../amazon/disco/agent/reflect/event/EventBus.java | 5 ----- 3 files changed, 1 insertion(+), 24 deletions(-) diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/ReflectiveCall.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/ReflectiveCall.java index 8e094b9..857d171 100644 --- a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/ReflectiveCall.java +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/ReflectiveCall.java @@ -143,13 +143,6 @@ public boolean methodFound() { public T call(Object... args) { String fullClassName = DISCO_AGENT_PACKAGE_ROOT + className; try { - Logger.debug("Trying to reflectively call " + fullClassName + ":" + methodName + " with parameter types:"); - Logger.debug("with arguments:"); - if (args != null) for (Object arg: args) { - if (arg != null) { - Logger.debug(String.valueOf(arg)); - } - } if (method == null) { createMethod(); @@ -259,15 +252,10 @@ public Class[] getArgTypes() { private void createMethod() { String fullClassName = DISCO_AGENT_PACKAGE_ROOT + className; try { - Logger.debug("Trying to reflectively create " + fullClassName + ":" + methodName + " with parameter types:"); - if (argTypes != null) for (Class clazz: argTypes) { - Logger.debug(clazz.getName()); - Logger.debug(clazz.getName()); - } Class clazz = Class.forName(fullClassName); method = clazz.getDeclaredMethod(methodName, argTypes); } catch (Throwable t) { - Logger.debug("Method " + fullClassName + ":" + methodName + " was not found."); + //do nothing } } } diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java index d691822..344774e 100644 --- a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/concurrent/TransactionContext.java @@ -39,7 +39,6 @@ public class TransactionContext { * @return the tx stack depth. */ public static int create() { - Logger.info("Creating a new random transactionId and an empty transaction Context."); return ReflectiveCall.returning(int.class) .ofClass(TRANSACTIONCONTEXT_CLASS) .ofMethod("create") @@ -54,7 +53,6 @@ public static int create() { * this will do nothing. */ public static void destroy() { - Logger.info("Creating a new random transactionId and an empty transaction Context."); ReflectiveCall.returningVoid() .ofClass(TRANSACTIONCONTEXT_CLASS) .ofMethod("destroy") @@ -65,7 +63,6 @@ public static void destroy() { * @param value - the value to set */ public static void set(String value) { - Logger.info("Setting transaction Id to" + value); ReflectiveCall.returningVoid() .ofClass(TRANSACTIONCONTEXT_CLASS) .ofMethod("set") @@ -90,7 +87,6 @@ public static String get() { * @param value the metadata value */ public static void putMetadata(String key, Object value) { - Logger.info("Putting metadata to key " + key); ReflectiveCall call = ReflectiveCall.returningVoid() .ofClass(TRANSACTIONCONTEXT_CLASS) .ofMethod("putMetadata") @@ -106,7 +102,6 @@ public static void putMetadata(String key, Object value) { * @param key the key of the metadata */ public static void removeMetadata(String key) { - Logger.info("Removing metadata with key " + key); ReflectiveCall call = ReflectiveCall.returningVoid() .ofClass(TRANSACTIONCONTEXT_CLASS) .ofMethod("removeMetadata") @@ -164,7 +159,6 @@ public static Map getMetadataWithTag(String tag) { * Clear the DiSCo TransactionContext to revert to its default value, or a no-op if Agent not loaded */ public static void clear() { - Logger.info("Clearing transaction context"); ReflectiveCall.returningVoid() .ofClass(TRANSACTIONCONTEXT_CLASS) .ofMethod("clear") diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/event/EventBus.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/event/EventBus.java index cdb96ea..3b19475 100644 --- a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/event/EventBus.java +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/reflect/event/EventBus.java @@ -27,7 +27,6 @@ public class EventBus { * @param e the event to publish */ static public void publish(Event e) { - Logger.info("Publishing event " + e + " from " + e.getOrigin()); ReflectiveCall.returningVoid() .ofClass(EVENTBUS_CLASS) .ofMethod("publish") @@ -40,7 +39,6 @@ static public void publish(Event e) { * @param l the listener to add */ static public void addListener(Listener l) { - Logger.info("Adding listener to event bus " + l + " with priority " + l.getPriority()); ReflectiveCall .returningVoid() .ofClass(EVENTBUS_CLASS) @@ -54,7 +52,6 @@ static public void addListener(Listener l) { * @param l the listener to remove. It is safe to remove a listener not currently added. */ static public void removeListener(Listener l) { - Logger.info("Removing listener from event bus " + l + " with priority " + l.getPriority()); ReflectiveCall .returningVoid() .ofClass(EVENTBUS_CLASS) @@ -67,7 +64,6 @@ static public void removeListener(Listener l) { * Remove all listeners from the EventBus, returning it to its initial state */ static public void removeAllListeners() { - Logger.info("Removing all listeners from event bus"); ReflectiveCall .returningVoid() .ofClass(EVENTBUS_CLASS) @@ -81,7 +77,6 @@ static public void removeAllListeners() { * @return true if the listener is presently registered to receive events */ static public boolean isListenerPresent(Listener l) { - Logger.info("Checking if Listener " + l + " is present "); Boolean returnValue = ReflectiveCall .returning(Boolean.class) .ofClass(EVENTBUS_CLASS) From 5347d3b2e3c2e9ee1d4ecd4e25b5ba538b187a86 Mon Sep 17 00:00:00 2001 From: William Armiros Date: Mon, 13 Jul 2020 12:00:41 -0500 Subject: [PATCH 23/45] initial commit of sql interceptor module --- README.md | 1 + disco-java-agent-sql/CODE_OF_CONDUCT.md | 4 + disco-java-agent-sql/CONTRIBUTING.md | 61 ++++++ disco-java-agent-sql/LICENSE | 202 ++++++++++++++++++ disco-java-agent-sql/NOTICE | 2 + disco-java-agent-sql/README.md | 34 +++ disco-java-agent-sql/build.gradle.kts | 27 +++ .../CODE_OF_CONDUCT.md | 4 + .../CONTRIBUTING.md | 61 ++++++ .../disco-java-agent-sql-plugin/LICENSE | 202 ++++++++++++++++++ .../disco-java-agent-sql-plugin/NOTICE | 2 + .../disco-java-agent-sql-plugin/README.md | 19 ++ .../build.gradle.kts | 72 +++++++ .../gradle.properties | 16 ++ .../amazon/disco/agent/integtest/sql/.gitkeep | 1 + disco-java-agent-sql/gradle.properties | 16 ++ .../amazon/disco/agent/sql/SQLSupport.java | 5 + .../software/amazon/disco/agent/sql/.gitkeep | 1 + settings.gradle.kts | 3 + 19 files changed, 733 insertions(+) create mode 100644 disco-java-agent-sql/CODE_OF_CONDUCT.md create mode 100644 disco-java-agent-sql/CONTRIBUTING.md create mode 100644 disco-java-agent-sql/LICENSE create mode 100644 disco-java-agent-sql/NOTICE create mode 100644 disco-java-agent-sql/README.md create mode 100644 disco-java-agent-sql/build.gradle.kts create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/CODE_OF_CONDUCT.md create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/CONTRIBUTING.md create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/LICENSE create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/NOTICE create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/README.md create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/gradle.properties create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/.gitkeep create mode 100644 disco-java-agent-sql/gradle.properties create mode 100644 disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SQLSupport.java create mode 100644 disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/.gitkeep diff --git a/README.md b/README.md index 8323485..9bbd729 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,7 @@ a few layers and families of projects in here: 1. Canonical Pluggable agent, capable of plugin discovery, in disco-java-agent/disco-java-agent 1. A facility to 'Inject' a Disco Agent into managed runtimes like AWS Lambda 1. A Plugin to support Servlets and Apache HTTP clients, in disco-java-agent-web-plugin +1. A Plugin to support SQL connections & queries using JDBC, in disco-java-agent-sql-plugin 1. Example code in anything with '-example' in the project name. 1. Tests in anything with '-test' in the project name. diff --git a/disco-java-agent-sql/CODE_OF_CONDUCT.md b/disco-java-agent-sql/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5b627cf --- /dev/null +++ b/disco-java-agent-sql/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent-sql/CONTRIBUTING.md b/disco-java-agent-sql/CONTRIBUTING.md new file mode 100644 index 0000000..5ad897b --- /dev/null +++ b/disco-java-agent-sql/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional +documentation, we greatly value feedback and contributions from our community. + +Please read through this document before submitting any issues or pull requests to ensure we have all the necessary +information to effectively respond to your bug report or contribution. + + +## Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features. + +When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already +reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of our code being used +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + + +## Contributing via Pull Requests +Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: + +1. You are working against the latest source on the *master* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - we would hate for your time to be wasted. + +To send us a pull request, please: + +1. Fork the repository. +2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. +3. Ensure local tests pass. +4. Commit to your fork using clear commit messages. +5. Send us a pull request, answering any default questions in the pull request interface. +6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + + +## Finding contributions to work on +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. + + +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. + + +## Security issue notifications +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. + + +## Licensing + +See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. + +We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent-sql/LICENSE b/disco-java-agent-sql/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/disco-java-agent-sql/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/disco-java-agent-sql/NOTICE b/disco-java-agent-sql/NOTICE new file mode 100644 index 0000000..fbd62dc --- /dev/null +++ b/disco-java-agent-sql/NOTICE @@ -0,0 +1,2 @@ +DiSCo +Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/disco-java-agent-sql/README.md b/disco-java-agent-sql/README.md new file mode 100644 index 0000000..dc65562 --- /dev/null +++ b/disco-java-agent-sql/README.md @@ -0,0 +1,34 @@ +## Disco 'SQL' Service Support + +Serving as both an example of how to author a Disco library/plugin, and also as a usable +Event producer for popular frameworks used in service oriented software, this subproject is laid out as follows: + +1. In this folder, the Installables to intercept SQL interactions, and issue appropriate Event Bus Events. +1. In the disco-java-agent-sql-plugin subfolder, a proper Disco plugin, bundled as a plugin JAR file with Manifest. + +## Package description + +TODO + +## Why the separation into two projects? + +Disco supports two models of development: + +1. Standalone, self-contained agents. +1. Pluggable, extensible agents. + +If you only want to support the first of these, there's no need to produce the plugin MANIFEST or +to shade/shadow your JAR file. The final build of the Agent itself would do that all in a single step. + +On the other hand, if you only wanted to support the Plugin model you could simply have all your source +code *and* your MANIFEST or MANIFEST-generation in a single project. + +For the purposes of this library, especially since it serves as an example, we support both mechanisms, +hence the factoring into a lib project and a plugin JAR project. + +You can see tests for both 'flavours' in the disco-java-agent-example-test project. Inside the +build.gradle.kts file there are two test targets + +1. The familiar default 'test' target, which tests via the standalone disco-java-agent-example agent +1. An extra 'testViaPlugin' test target, which tests using the disco-java-agent canonical agent, and the +built disco-java-agent-sql-plugin plugin. diff --git a/disco-java-agent-sql/build.gradle.kts b/disco-java-agent-sql/build.gradle.kts new file mode 100644 index 0000000..946f4af --- /dev/null +++ b/disco-java-agent-sql/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +dependencies { + implementation(project(":disco-java-agent:disco-java-agent-core")) + testImplementation("org.mockito", "mockito-core", "1.+") +} + +configure { + publications { + named("maven") { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/CODE_OF_CONDUCT.md b/disco-java-agent-sql/disco-java-agent-sql-plugin/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5b627cf --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/CONTRIBUTING.md b/disco-java-agent-sql/disco-java-agent-sql-plugin/CONTRIBUTING.md new file mode 100644 index 0000000..5ad897b --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional +documentation, we greatly value feedback and contributions from our community. + +Please read through this document before submitting any issues or pull requests to ensure we have all the necessary +information to effectively respond to your bug report or contribution. + + +## Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features. + +When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already +reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of our code being used +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + + +## Contributing via Pull Requests +Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: + +1. You are working against the latest source on the *master* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - we would hate for your time to be wasted. + +To send us a pull request, please: + +1. Fork the repository. +2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. +3. Ensure local tests pass. +4. Commit to your fork using clear commit messages. +5. Send us a pull request, answering any default questions in the pull request interface. +6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + + +## Finding contributions to work on +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. + + +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. + + +## Security issue notifications +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. + + +## Licensing + +See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. + +We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/LICENSE b/disco-java-agent-sql/disco-java-agent-sql-plugin/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/NOTICE b/disco-java-agent-sql/disco-java-agent-sql-plugin/NOTICE new file mode 100644 index 0000000..fbd62dc --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/NOTICE @@ -0,0 +1,2 @@ +DiSCo +Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/README.md b/disco-java-agent-sql/disco-java-agent-sql-plugin/README.md new file mode 100644 index 0000000..4b8f1f7 --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/README.md @@ -0,0 +1,19 @@ +## Disco 'SQL' Service Support Plugin + +This is a plugin built from the source in the folder above, including a build rule +to output a well-formed Disco plugin. + +### Manifest generation + +The build.gradle.kts file contains a build rule to generate an appropriate MANIFEST + +### Dependency shading + +Inherited from a top level build.gradle.kts file in the top level project, the ByteBuddy and ASM +dependencies are repackaged in agreement with the expectations of the disco-java-agent. + +### Integ Tests + +The test target in build.gradle.kts is configured to apply the disco-java-agent via an argument given to the +invocation of java, supplied to which is a pluginPath pointing to the output folder where the built +disco-java-agent-sql-plugin plugin JAR file can be found. Without both of these, the tests will fail. \ No newline at end of file diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts b/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts new file mode 100644 index 0000000..84265cb --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +plugins { + id("com.github.johnrengelman.shadow") +} + +dependencies { + // TODO: Refactor this block and other common plugin build logic to top-level build.gradle after deciding + // a safe way to check whether a subproject represents a plugin + runtimeOnly(project(":disco-java-agent-sql")) { + //by setting this flag false, we take only what is described by the above project, and not its entire + //closure of transitive dependencies (i.e. all of Core, all of Bytebuddy, etc) + //this makes our generated Jar minimal, containing only our source files, and our manifest. All those other + //dependencies are expected to be in the base agent, which loads this plugin. + //Ideally we would have a test for this which inspects the final Jar's content, but it can be reviewed manually + //on the command line with "jar -tf disco-java-agent-sql-plugin.jar" + isTransitive = false + } + + //Test target is integ tests for this plugin. Some classes in the integ tests also self-test via little unit tests during this + //testrun. + testImplementation(project(":disco-java-agent:disco-java-agent-api")) + testImplementation("org.mockito", "mockito-core", "1.+") +} + +tasks.shadowJar { + manifest { + attributes(mapOf( + "Disco-Installable-Classes" to "software.amazon.disco.agent.sql.SQLSupport" + )) + } +} + +val ver = project.version + +//integ testing needs a loaded agent, and the loaded plugin +tasks.test { + //explicitly remove the runtime classpath from the tests since they are integ tests, and may not access the + //dependency we acquired in order to build the plugin, namely the disco-java-agent-sql jar which makes reference + //to byte buddy classes which have NOT been relocated by a shadowJar rule. Discovering those unrelocated classes + //would not be possible in a real client installation, and would cause plugin loading to fail. + classpath = classpath.minus(configurations.runtimeClasspath.get()) + + //load the agent for the tests, and have it discover the web plugin + jvmArgs("-javaagent:../../disco-java-agent/disco-java-agent/build/libs/disco-java-agent-"+ver+".jar=pluginPath=./build/libs:extraverbose") + + //we do not take any normal compile/runtime dependency on this, but it must be built first since the above jvmArg + //refers to its built artifact. + dependsOn(":disco-java-agent:disco-java-agent:build") + dependsOn(":disco-java-agent-sql:disco-java-agent-sql-plugin:assemble") +} + +configure { + publications { + named("maven") { + artifact(tasks.jar.get()) + } + } +} diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/gradle.properties b/disco-java-agent-sql/disco-java-agent-sql-plugin/gradle.properties new file mode 100644 index 0000000..d861bc6 --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/gradle.properties @@ -0,0 +1,16 @@ +# +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# + +maven = true \ No newline at end of file diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/.gitkeep b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/.gitkeep new file mode 100644 index 0000000..8a05bb5 --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/.gitkeep @@ -0,0 +1 @@ +Integ tests TODO \ No newline at end of file diff --git a/disco-java-agent-sql/gradle.properties b/disco-java-agent-sql/gradle.properties new file mode 100644 index 0000000..d861bc6 --- /dev/null +++ b/disco-java-agent-sql/gradle.properties @@ -0,0 +1,16 @@ +# +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# + +maven = true \ No newline at end of file diff --git a/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SQLSupport.java b/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SQLSupport.java new file mode 100644 index 0000000..c897f1f --- /dev/null +++ b/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SQLSupport.java @@ -0,0 +1,5 @@ +package software.amazon.disco.agent.sql; + +public class SQLSupport { + // TODO - implement SQL Interceptor +} diff --git a/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/.gitkeep b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/.gitkeep new file mode 100644 index 0000000..216c279 --- /dev/null +++ b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/.gitkeep @@ -0,0 +1 @@ +Unit tests TODO \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c01ea18..02650f0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,9 @@ include("disco-java-agent:disco-java-agent-inject-api") include("disco-java-agent-web") include("disco-java-agent-web:disco-java-agent-web-plugin") +include("disco-java-agent-sql") +include("disco-java-agent-sql:disco-java-agent-sql-plugin") + include("disco-java-agent-example") include("disco-java-agent-example-test") include("disco-java-agent-example-injector-test") \ No newline at end of file From b80a3bdad656c52b4025a984ec3ec5d20e8a7513 Mon Sep 17 00:00:00 2001 From: Hongbo Liu Date: Thu, 16 Jul 2020 20:05:31 -0400 Subject: [PATCH 24/45] initial commit of disco instrumentation preprocess library --- .../build.gradle.kts | 26 ++ .../lombok.config | 2 + .../preprocess/cli/Driver.java | 42 +++ .../preprocess/cli/PreprocessConfig.java | 38 +++ .../cli/PreprocessConfigParser.java | 224 +++++++++++++++ .../AgentLoaderNotProvidedException.java | 33 +++ ...umentationStateSerializationException.java | 33 +++ .../exceptions/ModuleExportException.java | 34 +++ .../ModuleLoaderNotProvidedException.java | 33 +++ .../NoModuleToInstrumentException.java | 30 ++ .../exceptions/NoPathProvidedException.java | 33 +++ .../UnableToReadJarEntryException.java | 20 ++ .../export/JarModuleExportStrategy.java | 248 ++++++++++++++++ .../export/ModuleExportStrategy.java | 37 +++ .../InstrumentedClassState.java | 54 ++++ .../instrumentation/ModuleTransformer.java | 108 +++++++ .../TransformationListener.java | 90 ++++++ .../loaders/agents/AgentLoader.java | 29 ++ .../loaders/agents/DiscoAgentLoader.java | 49 ++++ .../loaders/modules/JarModuleLoader.java | 200 +++++++++++++ .../loaders/modules/ModuleInfo.java | 37 +++ .../loaders/modules/ModuleLoader.java | 31 ++ .../preprocess/util/PreprocessConstants.java | 9 + .../cli/PreprocessConfigParserTest.java | 131 +++++++++ .../export/JarModuleExportStrategyTest.java | 267 ++++++++++++++++++ .../InstrumentedClassStateTest.java | 44 +++ .../ModuleTransformerTest.java | 153 ++++++++++ .../TransformationListenerTest.java | 62 ++++ .../loaders/agents/DiscoAgentLoaderTest.java | 26 ++ .../loaders/modules/JarModuleLoaderTest.java | 201 +++++++++++++ .../preprocess/util/MockEntities.java | 134 +++++++++ settings.gradle.kts | 3 +- 32 files changed, 2460 insertions(+), 1 deletion(-) create mode 100644 disco-java-agent-instrumentation-preprocess/build.gradle.kts create mode 100644 disco-java-agent-instrumentation-preprocess/lombok.config create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/AgentLoaderNotProvidedException.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InstrumentationStateSerializationException.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleExportException.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleLoaderNotProvidedException.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoModuleToInstrumentException.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoPathProvidedException.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToReadJarEntryException.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/InstrumentedClassState.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/AgentLoader.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/PreprocessConstants.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/InstrumentedClassStateTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java diff --git a/disco-java-agent-instrumentation-preprocess/build.gradle.kts b/disco-java-agent-instrumentation-preprocess/build.gradle.kts new file mode 100644 index 0000000..0f9753c --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/build.gradle.kts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +plugins { + id("io.freefair.lombok") version "5.1.0" +} + +dependencies { + implementation(project(":disco-java-agent:disco-java-agent-core")) + implementation(project(":disco-java-agent:disco-java-agent-api")) + implementation(project(":disco-java-agent:disco-java-agent-inject-api")) + + implementation("org.apache.logging.log4j", "log4j-core", "2.13.3") +} \ No newline at end of file diff --git a/disco-java-agent-instrumentation-preprocess/lombok.config b/disco-java-agent-instrumentation-preprocess/lombok.config new file mode 100644 index 0000000..6aa51d7 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java new file mode 100644 index 0000000..ed322d9 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.cli; + +import software.amazon.disco.instrumentation.preprocess.instrumentation.ModuleTransformer; +import software.amazon.disco.instrumentation.preprocess.loaders.agents.DiscoAgentLoader; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.JarModuleLoader; + +/** + * Entry point of the library. A {@link ModuleTransformer} instance is being created to orchestrate the instrumentation + * process of all packages supplied. + */ +public class Driver { + public static void main(String[] args) { + final PreprocessConfig config = new PreprocessConfigParser().parseCommandLine(args); + + if(config == null){ + System.exit(1); + } + + ModuleTransformer.builder() + .agentLoader(new DiscoAgentLoader(config.getAgentPath())) + .jarLoader(new JarModuleLoader(config.getJarPaths())) + .suffix(config.getSuffix()) + .logLevel(config.getLogLevel()) + .build() + .transform(); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java new file mode 100644 index 0000000..42a2f7f --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.cli; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; +import org.apache.logging.log4j.Level; + +import java.util.List; + +/** + * Container for the config created from the command line args + */ +@Builder +@Getter +public class PreprocessConfig { + private final String outputDir; + @Singular + private final List jarPaths; + private final String agentPath; + private final String suffix; + private final Level logLevel; + private final String serializationJarPath; +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java new file mode 100644 index 0000000..9ffe73e --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java @@ -0,0 +1,224 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.cli; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.logging.log4j.Level; + +import java.util.HashMap; +import java.util.Map; + +/** + * Parses command line arguments supplied to the preprocess tool. + */ +public class PreprocessConfigParser { + private static final Map ACCEPTED_FLAGS = new HashMap(); + + /** + * Parses the command line arguments supplied to the library. + * Transformed package will override original file if NO destination And (prefix OR suffix) are supplied. + * + * @param args arguments passed to be parsed + * @return an instance of {@link PreprocessConfig}, null if format of the args is invalid + */ + public PreprocessConfig parseCommandLine(String[] args) { + if (args == null || args.length == 0) { + System.err.println("Mandatory options not supplied, please use [--help] to get a list of all options supported by this CLI."); + return null; + } + + // only print help text if it's the first argument passed in and ignores all other args + if (args[0].toLowerCase().equals("--help")) { + printHelpText(); + return null; + } + + setupAcceptedFlags(); + PreprocessConfig.PreprocessConfigBuilder builder = PreprocessConfig.builder(); + + OptionToMatch flagBeingMatched = null; + + for (String arg : args) { + final String argLowered = arg.toLowerCase(); + + if (flagBeingMatched == null) { + // no previous flag found, expecting a flag + + if (!ACCEPTED_FLAGS.containsKey(argLowered)) { + System.err.println("Flag: [" + arg + "] is invalid"); + return null; + } + + final OptionToMatch option = ACCEPTED_FLAGS.get(argLowered); + if (option.hasArgument) { + flagBeingMatched = ACCEPTED_FLAGS.get(argLowered); + } else { + processFlagWithNoArg(argLowered, builder); + } + } else { + // previous flag still expecting an argument but another flag is discovered + if (ACCEPTED_FLAGS.containsKey(argLowered) && !flagBeingMatched.isMatched()) { + System.err.println("Flag: [" + flagBeingMatched + "] requires an argument"); + return null; + } + + // a previously detected option that accepts multi values is now finished matching its arguments. + // and a new option is now being matched + if (ACCEPTED_FLAGS.containsKey(argLowered) && flagBeingMatched.isMatched()) { + final OptionToMatch option = ACCEPTED_FLAGS.get(argLowered); + + if (option.hasArgument) { + flagBeingMatched = option; + } else { + processFlagWithNoArg(argLowered, builder); + } + continue; + } + + if (flagBeingMatched.hasArgument) { + flagBeingMatched = matchArgWithFlag(flagBeingMatched, arg, builder); + } + } + } + + // the last flag discovered is missing its arg + if (flagBeingMatched != null) { + System.err.println("Flag: [" + flagBeingMatched + "] requires an argument"); + return null; + } + + return builder.build(); + } + + /** + * Setups the map that contains all the accepted options of this CLI. + */ + protected void setupAcceptedFlags() { + ACCEPTED_FLAGS.put("--help", new OptionToMatch("--help", false, false)); + ACCEPTED_FLAGS.put("--verbose", new OptionToMatch("--verbose", false, false)); + ACCEPTED_FLAGS.put("--silent", new OptionToMatch("--silent", false, false)); + + ACCEPTED_FLAGS.put("--outputdir", new OptionToMatch("--outputdir", true, false)); + ACCEPTED_FLAGS.put("--jarpaths", new OptionToMatch("--jarpaths", true, true)); + ACCEPTED_FLAGS.put("--agentpath", new OptionToMatch("--agentpath", true, false)); + ACCEPTED_FLAGS.put("--serializationpath", new OptionToMatch("--serializationpath", true, false)); + ACCEPTED_FLAGS.put("--suffix", new OptionToMatch("--suffix", true, false)); + + ACCEPTED_FLAGS.put("-out", new OptionToMatch("-out", true, false)); + ACCEPTED_FLAGS.put("-jps", new OptionToMatch("-jps", true, true)); + ACCEPTED_FLAGS.put("-ap", new OptionToMatch("-ap", true, false)); + ACCEPTED_FLAGS.put("-sp", new OptionToMatch("-sp", true, false)); + ACCEPTED_FLAGS.put("-suf", new OptionToMatch("-suf", true, false)); + } + + /** + * Prints out the help text when the [--help] option is passed. + */ + protected void printHelpText() { + System.out.println("Disco Instrumentation Preprocess Library Command Line Interface\n" + + "\t Usage: [options] \n" + + "\t\t --help List all supported options supported by the CLI.\n" + + "\t\t --outputDir | -out \n" + + "\t\t --jarPaths | -jps \n" + + "\t\t --serializationPath | -sp \n" + + "\t\t --agentPath | -ap \n" + + "\t\t --suffix | -suf \n" + + "\t\t --verbose Set the log level to log everything.\n" + + "\t\t --silent Disable logging to the console.\n\n" + + "The default behavior of the library will replace the original package scheduled for instrumentation if NO destination AND suffix are supplied.\n" + + "An agent AND either a servicePackage or at least one dependenciesPath MUST be supplied." + ); + } + + /** + * Matches the argument to the previously discovered flag. eg: [-out ]. + * + * @param option a valid option to be matched with an argument + * @param argument argument to be matched to the flag + * @param builder {@link PreprocessConfig.PreprocessConfigBuilder builder} to build the {@link PreprocessConfig} + * @return the same {@link OptionToMatch} instance if the option accepts multiple values, null if option only accepts one value. + */ + protected OptionToMatch matchArgWithFlag(OptionToMatch option, String argument, PreprocessConfig.PreprocessConfigBuilder builder) { + OptionToMatch result = null; + + switch (option.getFlag().toLowerCase()) { + case "-out": + case "--outputdir": + builder.outputDir(argument); + break; + case "--serializationpath": + case "-sp": + builder.serializationJarPath(argument); + break; + case "-jps": + case "--jarpaths": + builder.jarPath(argument); + option.isMatched = true; + result = option; + break; + case "-ap": + case "--agentpath": + builder.agentPath(argument); + break; + case "-suf": + case "--suffix": + builder.suffix(argument); + break; + default: + // will never be invoked since flags are already validated. + } + + return result; + } + + /** + * Matches the argument to the previously discovered flag. eg: [-out ]. + * + * @param flag a valid previously discovered flag + * @param builder {@link PreprocessConfig.PreprocessConfigBuilder builder} to build the {@link PreprocessConfig} + */ + protected void processFlagWithNoArg(String flag, PreprocessConfig.PreprocessConfigBuilder builder) { + switch (flag.toLowerCase()) { + case "--help": + // ignore this flag since its not supplied as the first arg. + break; + case "--verbose": + builder.logLevel(Level.TRACE); + break; + case "--silent": + builder.logLevel(Level.OFF); + break; + default: + // will never be invoked since flags are already validated. + } + } + + /** + * A struct that describes a valid option. + */ + @AllArgsConstructor() + @RequiredArgsConstructor() + @Getter + class OptionToMatch { + final String flag; + final boolean hasArgument; + final boolean acceptsMultiValues; + + boolean isMatched; + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/AgentLoaderNotProvidedException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/AgentLoaderNotProvidedException.java new file mode 100644 index 0000000..e42bc73 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/AgentLoaderNotProvidedException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.instrumentation.ModuleTransformer; +import software.amazon.disco.instrumentation.preprocess.loaders.agents.AgentLoader; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when a valid {@link AgentLoader} is not provided to + * {@link ModuleTransformer} + */ +public class AgentLoaderNotProvidedException extends RuntimeException { + /** + * Constructor invoking the parent constructor with a fixed error message + */ + public AgentLoaderNotProvidedException() { + super(PreprocessConstants.MESSAGE_PREFIX + "Agent loader not provided"); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InstrumentationStateSerializationException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InstrumentationStateSerializationException.java new file mode 100644 index 0000000..4717278 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InstrumentationStateSerializationException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when failed to serialize instrumentation meta-data to Jar + */ +public class InstrumentationStateSerializationException extends RuntimeException { + + /** + * Constructor + * + * @param cause {@link Throwable cause} of the failure for tracing the root cause. + */ + public InstrumentationStateSerializationException(Throwable cause) { + super(PreprocessConstants.MESSAGE_PREFIX + "Failed to serialize instrumentation state object to Jar", cause); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleExportException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleExportException.java new file mode 100644 index 0000000..d6f0bf8 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleExportException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when an error has occurred during the export process + */ +public class ModuleExportException extends RuntimeException { + /** + * Constructor that accepts a message explaining why the module export process failed as well as + * a {@link Throwable} instance for tracing. + * + * @param message cause of the failure + * @param cause {@link Throwable cause} of the failure for tracing the root cause. + */ + public ModuleExportException(String message, Throwable cause) { + super(PreprocessConstants.MESSAGE_PREFIX + message, cause); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleLoaderNotProvidedException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleLoaderNotProvidedException.java new file mode 100644 index 0000000..2c0f9f9 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleLoaderNotProvidedException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.instrumentation.ModuleTransformer; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleLoader; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when a valid {@link ModuleLoader} is not provided to + * {@link ModuleTransformer} + */ +public class ModuleLoaderNotProvidedException extends RuntimeException { + /** + * Constructor invoking the parent constructor with a fixed error message + */ + public ModuleLoaderNotProvidedException() { + super(PreprocessConstants.MESSAGE_PREFIX + "package loader not provided"); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoModuleToInstrumentException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoModuleToInstrumentException.java new file mode 100644 index 0000000..ef751db --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoModuleToInstrumentException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when no packages are found to be instrumented under the paths specified. + */ +public class NoModuleToInstrumentException extends RuntimeException { + /** + * Constructor invoking the parent constructor with a fixed error message + */ + public NoModuleToInstrumentException() { + super(PreprocessConstants.MESSAGE_PREFIX + "No modules have been loaded to be instrumented"); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoPathProvidedException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoPathProvidedException.java new file mode 100644 index 0000000..ae48155 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoPathProvidedException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.loaders.agents.AgentLoader; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleLoader; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when initializing {@link ModuleLoader} + * or {@link AgentLoader} with no path provided. + */ +public class NoPathProvidedException extends RuntimeException { + /** + * Constructor invoking the parent constructor with a fixed error message + */ + public NoPathProvidedException() { + super(PreprocessConstants.MESSAGE_PREFIX + "No path provided to load agent or package"); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToReadJarEntryException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToReadJarEntryException.java new file mode 100644 index 0000000..8bcc473 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToReadJarEntryException.java @@ -0,0 +1,20 @@ +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when the {@link ModuleExportStrategy exporter} + * fails to read an exiting entry from the original Jar file. + */ +public class UnableToReadJarEntryException extends RuntimeException { + /** + * Constructor + * + * @param entryName {@link java.util.jar.JarEntry} that failed to be copied + * @param cause {@link Throwable cause} of the failure for tracing the root cause. + */ + public UnableToReadJarEntryException(String entryName, Throwable cause) { + super(PreprocessConstants.MESSAGE_PREFIX + "Failed to read Jar entry: " + entryName, cause); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java new file mode 100644 index 0000000..b041f7e --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java @@ -0,0 +1,248 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.export; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleExportException; +import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; +import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; +import software.amazon.disco.instrumentation.preprocess.serialization.InstrumentationState; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Enumeration; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; + +/** + * Strategy to export transformed classes to a local Jar + */ +public class JarModuleExportStrategy implements ModuleExportStrategy { + private static final Logger log = LogManager.getLogger(JarModuleExportStrategy.class); + private static Path tempDir = null; + + private final String outputDir; + + /** + * Constructor with the destination path provided. + * + * @param outputDir Absolute output path of the transformed jar. Default is the current folder where + * the original jar is located. + */ + public JarModuleExportStrategy(final String outputDir) { + this.outputDir = outputDir; + } + + /** + * Constructor with no destination path provided. Transformed jar will replace the original file. + */ + public JarModuleExportStrategy() { + this.outputDir = null; + } + + /** + * Exports all transformed classes to a Jar file. A temporary Jar File will be created to store all + * the transformed classes and then be renamed to replace the original Jar. + * + * @param moduleInfo Information of the original Jar + * @param instrumented a map of instrumented classes with their bytecode + * @param instrumentationState state to be passed to the runtime agent to dynamically skip already instrumented classes + * @param suffix suffix of the transformed package + */ + @Override + public void export(final ModuleInfo moduleInfo, final Map instrumented, final InstrumentationState instrumentationState, final String suffix) { + log.debug(PreprocessConstants.MESSAGE_PREFIX + "Saving changes to Jar"); + + final File file = createTempFile(moduleInfo); + + buildOutputJar(moduleInfo, instrumented, file); + moveTempFileToDestination(moduleInfo, suffix, file); + } + + /** + * Creates a Temporary file to store transformed classes and existing {@link JarEntry entries} from + * the original {@link JarFile}. A temp folder with a 'disco' suffix will be created where these temp files will be stored if {@link #tempDir} is null. + * A default suffix of '.temp' is used as implemented by the {@link Files} api. + * + * @param moduleInfo Information of the original Jar + * @return created temp file + */ + protected File createTempFile(final ModuleInfo moduleInfo) { + try { + if (tempDir == null) { + tempDir = Files.createTempDirectory("disco"); + } + + return Files.createTempFile(tempDir, moduleInfo.getJarFile().getName(), null).toFile(); + } catch (IOException e) { + throw new ModuleExportException("Failed to create temp Jar file", e); + } + } + + /** + * Inserts all transformed classes into the temporary Jar file + * + * @param jarOS {@link JarOutputStream} used to write entries to the output jar + * @param instrumented a map of instrumented classes with their bytecode + */ + protected void saveTransformedClasses(final JarOutputStream jarOS, final Map instrumented) { + for (Map.Entry mapEntry : instrumented.entrySet()) { + final String classPath = mapEntry.getKey(); + final InstrumentedClassState info = mapEntry.getValue(); + final JarEntry entry = new JarEntry(classPath + ".class"); + + try { + jarOS.putNextEntry(entry); + jarOS.write(info.getClassBytes()); + jarOS.closeEntry(); + + //todo: implemented serialization of InstrumentationState + } catch (IOException e) { + throw new ModuleExportException(classPath, e); + } + } + } + + /** + * Copies existing entries from the original Jar to the temporary Jar while skipping any + * transformed classes. + * + * @param jarOS {@link JarOutputStream} used to write entries to the output jar + * @param moduleInfo Information of the original Jar + * @param instrumented a map of instrumented classes with their bytecode + */ + protected void copyExistingJarEntries(final JarOutputStream jarOS, final ModuleInfo moduleInfo, final Map instrumented) { + for (Enumeration entries = moduleInfo.getJarFile().entries(); entries.hasMoreElements(); ) { + final JarEntry entry = (JarEntry) entries.nextElement(); + final String keyToCheck = entry.getName().endsWith(".class") ? entry.getName().substring(0, entry.getName().lastIndexOf(".class")) : entry.getName(); + + try { + if (!instrumented.containsKey(keyToCheck)) { + if (entry.isDirectory()) { + jarOS.putNextEntry(entry); + } else { + copyJarEntry(jarOS, moduleInfo.getJarFile(), entry); + } + } + } catch (IOException e) { + throw new ModuleExportException("Failed to copy class: " + entry.getName(), e); + } + } + } + + /** + * Builds the output jar by copying existing entries from the original and inserting transformed classes + * + * @param moduleInfo Information of the original Jar + * @param instrumented a map of instrumented classes with their bytecode + * @param tempFile file that the JarOutputStream will write to + */ + protected void buildOutputJar(final ModuleInfo moduleInfo, final Map instrumented, final File tempFile) { + try (JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(tempFile))) { + copyExistingJarEntries(jarOS, moduleInfo, instrumented); + saveTransformedClasses(jarOS, instrumented); + } catch (IOException e) { + throw new ModuleExportException("Failed to create output Jar file", e); + } + } + + /** + * Copies a single {@link JarEntry entry} from the original Jar to the Temp Jar + * + * @param jarOS {@link JarOutputStream} used to write entries to the output jar + * @param file original {@link File jar} where the entry's binary data will be read + * @param entry a single {@link JarEntry Jar entry} + * @throws IOException + */ + protected void copyJarEntry(final JarOutputStream jarOS, final JarFile file, final JarEntry entry) { + try { + final InputStream entryStream = file.getInputStream(entry); + + if (entryStream == null) { + throw new UnableToReadJarEntryException(entry.getName(), null); + } + + jarOS.putNextEntry(entry); + + final byte[] buffer = new byte[1024]; + + int bytesRead; + while ((bytesRead = entryStream.read(buffer)) != -1) { + jarOS.write(buffer, 0, bytesRead); + } + jarOS.closeEntry(); + + } catch (IOException e) { + throw new UnableToReadJarEntryException(entry.getName(), e); + } + } + + /** + * Move the temp file containing all existing entries and transformed classes to the + * destination. If {@link #outputDir} is NOT specified, the original file will be replaced. + * + * @param moduleInfo Information of the original Jar + * @param suffix suffix to be appended to the transformed package + * @param tempFile output file to be moved to the destination path + * @return {@link Path} of the overwritten file + */ + protected Path moveTempFileToDestination(final ModuleInfo moduleInfo, final String suffix, final File tempFile) { + try { + final String destinationStr = outputDir == null ? + moduleInfo.getFile().getAbsolutePath() + : outputDir + "/" + moduleInfo.getFile().getName(); + + final String destinationStrWithSuffix = suffix == null ? + destinationStr + : destinationStr.substring(0, destinationStr.lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + suffix + PreprocessConstants.JAR_EXTENSION; + + final Path destination = Paths.get(destinationStrWithSuffix); + destination.toFile().getParentFile().mkdirs(); + + final boolean isOverride = outputDir == null && suffix == null; + + if (!isOverride && destination.toFile().exists()) { + throw new ModuleExportException("Failed move transformed package to output directory, package with same name already present: " + destinationStrWithSuffix, null); + } + + final Path filePath = Files.move( + Paths.get(tempFile.getAbsolutePath()), + destination, + StandardCopyOption.REPLACE_EXISTING); + + if (filePath == null) { + throw new ModuleExportException("Failed to replace existing jar file", null); + } + + log.debug(PreprocessConstants.MESSAGE_PREFIX + "All transformed classes saved"); + + return filePath; + } catch (IOException e) { + throw new ModuleExportException("Failed to replace existing jar file", e); + } + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java new file mode 100644 index 0000000..0bd309e --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.export; + +import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; +import software.amazon.disco.instrumentation.preprocess.serialization.InstrumentationState; + +import java.util.Map; + +/** + * Interface for the strategy to use when exporting transformed classes + */ +public interface ModuleExportStrategy { + /** + * Strategy called to export all transformed classes. + * + * @param info Information of the original Jar + * @param instrumented a map of instrumented classes with their bytecode + * @param instrumentationState state to be passed to the runtime agent to dynamically skip already instrumented classes + * @param suffix suffix to be appended to the transformed package + */ + void export(final ModuleInfo info, final Map instrumented, final InstrumentationState instrumentationState, final String suffix); +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/InstrumentedClassState.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/InstrumentedClassState.java new file mode 100644 index 0000000..1eaf407 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/InstrumentedClassState.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.instrumentation; + +import lombok.Getter; +import software.amazon.disco.agent.interception.Installable; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Class that encapsulates data of an instrumented class by ByteBuddy + */ +@Getter +public class InstrumentedClassState { + private final Set installableIds; + private byte[] classBytes; + + /** + * Object is created at the first instance when a class is transformed. + * + * @param installableId id of the {@link Installable installables} applied + * @param classBytes byte[] of the transformed class + */ + public InstrumentedClassState(final String installableId, final byte[] classBytes) { + this.classBytes = classBytes; + this.installableIds = new HashSet<>(Arrays.asList(installableId)); + } + + /** + * Updates the byte[] of an already transformed class as well as the set of installable ids. + * + * @param installableId id of the {@link Installable installables} applied + * @param classBytes byte[] of the transformed class + */ + public void update(final String installableId, final byte[] classBytes) { + this.classBytes = classBytes; + this.installableIds.add(installableId); + } +} \ No newline at end of file diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java new file mode 100644 index 0000000..f093b46 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java @@ -0,0 +1,108 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.instrumentation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.config.Configurator; +import software.amazon.disco.instrumentation.preprocess.exceptions.AgentLoaderNotProvidedException; +import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleLoaderNotProvidedException; +import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.loaders.agents.AgentLoader; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleLoader; +import software.amazon.disco.instrumentation.preprocess.serialization.InstrumentationState; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +import java.util.Map; + +/** + * Class responsible to orchestrate the instrumentation process involving agent loading, package loading, instrumentation + * triggering and exporting the transformed classes. + *

+ * At least one valid {@link AgentLoader} AND one {@link ModuleLoader} must be provided (either a service package loader + * or a dependency package loader). + */ +@Builder +@AllArgsConstructor +public class ModuleTransformer { + private static final Logger log = LogManager.getLogger(ModuleTransformer.class); + + private final ModuleLoader jarLoader; + private final AgentLoader agentLoader; + private final InstrumentationState instrumentationState; + private final String suffix; + private final Level logLevel; + + /** + * This method initiates the transformation process of all packages found under the provided paths. + */ + public void transform() { + if (logLevel == null) { + Configurator.setRootLevel(Level.INFO); + }else{ + Configurator.setRootLevel(logLevel); + } + + if (agentLoader == null) throw new AgentLoaderNotProvidedException(); + + agentLoader.loadAgent(); + + if (jarLoader == null) { + throw new ModuleLoaderNotProvidedException(); + } + + // Apply instrumentation on all jars + for (final ModuleInfo info : jarLoader.loadPackages()) { + applyInstrumentation(info); + //todo: store serialized instrumentation state to target jar + } + } + + /** + * Triggers instrumentation of classes using Reflection and applies the changes according to the + * {@link ModuleExportStrategy export strategy} + * of this package + * + * @param moduleInfo a package containing classes to be instrumented + */ + protected void applyInstrumentation(final ModuleInfo moduleInfo) { + for (String name : moduleInfo.getClassNames()) { + try { + Class.forName(name); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + log.warn(PreprocessConstants.MESSAGE_PREFIX + "Failed to initialize class:" + name, e); + } + } + + moduleInfo.getExportStrategy().export(moduleInfo, getInstrumentedClasses(), instrumentationState, suffix); + + // empty the map in preparation for transforming another package + getInstrumentedClasses().clear(); + } + + /** + * Fetches instrumented classes from the listener attached to all {@link software.amazon.disco.agent.interception.Installable installables}. + * + * @return a Map of class name as key and {@link InstrumentedClassState} as value + */ + protected Map getInstrumentedClasses() { + return TransformationListener.getInstrumentedTypes(); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java new file mode 100644 index 0000000..1f83a5a --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java @@ -0,0 +1,90 @@ +package software.amazon.disco.instrumentation.preprocess.instrumentation; + +import lombok.Getter; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +import java.util.HashMap; +import java.util.Map; + +/** + * This listener collects all the {@link AgentBuilder.Listener#onTransformation(TypeDescription, ClassLoader, JavaModule, boolean, DynamicType) events} + * and stores the byte[] of the transformed classes inside a map that is to be retrieved by the preprocess tool to perform static instrumentation. + */ +public class TransformationListener implements AgentBuilder.Listener { + @Getter + private final static Map instrumentedTypes = new HashMap<>(); + private final static Logger log = LogManager.getLogger(TransformationListener.class); + private final String uid; + + /** + * Constructor that takes in the type of the {@link software.amazon.disco.agent.interception.Installable installable} + * that this listener is attached to. + * + * @param uid unique identifier of the installable + */ + public TransformationListener(final String uid) { + this.uid = uid; + } + + /** + * {@inheritDoc} + */ + public void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) { + } + + /** + * {@inheritDoc} + *

+ * This event is intercepted by this listener which extracts the byte[] of the transformed classes and any + * auxiliary classes created as a result of the instrumentation. + */ + public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, DynamicType dynamicType) { + collectDataFromEvent(typeDescription, dynamicType); + } + + /** + * {@inheritDoc} + */ + public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded) { + } + + /** + * {@inheritDoc} + */ + public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) { + log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to instrument: " + typeName, throwable); + } + + /** + * {@inheritDoc} + */ + public void onComplete(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) { + } + + /** + * Extracts the byte[] of the transformed classes from the onTransformation events. Also updates byte[] + * of a already transformed class that underwent another transformation. + * + * @param typeDescription The type that is being transformed. + * @param dynamicType The {@link DynamicType dynamic type} that was created by ByteBuddy. + */ + protected void collectDataFromEvent(TypeDescription typeDescription, DynamicType dynamicType) { + if (instrumentedTypes.containsKey(typeDescription.getInternalName())) { + instrumentedTypes.get(typeDescription.getInternalName()).update(uid, dynamicType.getBytes()); + } else { + instrumentedTypes.put(typeDescription.getInternalName(), new InstrumentedClassState(uid, dynamicType.getBytes())); + } + + if (!dynamicType.getAuxiliaryTypes().isEmpty()) { + for(Map.Entry auxiliaryEntry : dynamicType.getAuxiliaryTypes().entrySet()){ + instrumentedTypes.put(auxiliaryEntry.getKey().getInternalName(), new InstrumentedClassState(null, auxiliaryEntry.getValue())); + } + } + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/AgentLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/AgentLoader.java new file mode 100644 index 0000000..f83ebcb --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/AgentLoader.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.agents; + +import software.amazon.disco.agent.interception.Installable; + +/** + * Agent loader interface that loads Java Agents and {@link Installable} + */ +public interface AgentLoader { + + /** + * load and install the agent dynamically at runtime. + */ + void loadAgent(); +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java new file mode 100644 index 0000000..9d9ff64 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.agents; + +import software.amazon.disco.agent.inject.Injector; +import software.amazon.disco.instrumentation.preprocess.exceptions.NoPathProvidedException; + +/** + * Agent loader used to dynamically load a Java Agent at runtime by calling the + * {@link Injector} api. + */ +public class DiscoAgentLoader implements AgentLoader { + protected String path; + + /** + * Constructor + * + * @param path path of the agent to be loaded + */ + public DiscoAgentLoader(final String path) { + if (path == null) { + throw new NoPathProvidedException(); + } + this.path = path; + } + + /** + * {@inheritDoc} + * Install a monolithic agent by directly invoking the {@link Injector} api. + */ + @Override + public void loadAgent() { + Injector.loadAgent(path, null); + } +} + diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java new file mode 100644 index 0000000..f09d4cc --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java @@ -0,0 +1,200 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.modules; + +import lombok.Getter; +import software.amazon.disco.agent.inject.Injector; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; +import software.amazon.disco.instrumentation.preprocess.exceptions.NoPathProvidedException; +import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * A {@link ModuleLoader} that loads all Jar files under specified paths + */ +public class JarModuleLoader implements ModuleLoader { + private static final Logger log = LogManager.getLogger(JarModuleLoader.class); + + @Getter + private final Set paths; + + @Getter + private final ModuleExportStrategy strategy; + + /** + * Constructor that sets default package export strategy as {@link JarModuleExportStrategy} + * + * @param paths list of paths to load Jar files + */ + public JarModuleLoader(final List paths) throws NoPathProvidedException { + if (paths == null || paths.size() == 0) throw new NoPathProvidedException(); + this.paths = new HashSet<>(paths); + this.strategy = new JarModuleExportStrategy(); + } + + /** + * Constructor + * + * @param strategy {@link ModuleExportStrategy strategy} for exporting transformed classes under this path. Default strategy is {@link JarModuleExportStrategy} + * @param paths list of paths to load Jar files + */ + public JarModuleLoader(final ModuleExportStrategy strategy, final List paths) throws NoPathProvidedException { + if (paths == null || paths.size() == 0) throw new NoPathProvidedException(); + this.paths = new HashSet<>(paths); + this.strategy = strategy; + } + + /** + * {@inheritDoc} + */ + @Override + public List loadPackages() { + final List packageEntries = new ArrayList<>(); + + for (String path : paths) { + for (File file : discoverFilesInPath(path)) { + final ModuleInfo info = loadPackage(file); + + if (info != null) { + packageEntries.add(info); + } + } + } + if (packageEntries.isEmpty()) { + throw new NoModuleToInstrumentException(); + } + return packageEntries; + } + + /** + * Discovers all files under a path + * + * @return a List of {@link File files}, empty is no files found + */ + protected List discoverFilesInPath(final String path) { + final List files = new ArrayList<>(); + final File packageDir = new File(path); + + final File[] packageFiles = packageDir.listFiles(); + + if (packageFiles == null) { + log.debug(PreprocessConstants.MESSAGE_PREFIX + "No packages found under path: " + path); + return files; + } + + files.addAll(Arrays.asList(packageFiles)); + return files; + } + + /** + * Helper method to load one single Jar file + * + * @param file Jar file to be loaded + * @return {@link ModuleInfo object} containing package data, null if file is not a valid {@link JarFile} + */ + protected ModuleInfo loadPackage(final File file) { + final JarFile jarFile = processFile(file); + + if (jarFile == null) return null; + + final List names = new ArrayList<>(); + + for (JarEntry entry : extractEntries(jarFile)) { + names.add(entry.getName().substring(0, entry.getName().lastIndexOf(".class")).replace('/', '.')); + } + + return names.isEmpty() ? null : new ModuleInfo(file, jarFile, names, strategy); + } + + /** + * Helper method that iterates and extracts {@link JarEntry entries} that are class files + * + * @param jarFile Jar to explore + * @return a list of {@link JarEntry entries} that are class files + */ + protected List extractEntries(final JarFile jarFile) { + final List result = new ArrayList<>(); + + if (jarFile != null) { + final Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry e = entries.nextElement(); + + if (!e.isDirectory() && e.getName().endsWith(".class")) { + result.add(e); + } + } + } + return result; + } + + /** + * Validates the file and adds it to the system class path + * + * @param file file to process + * @return a valid {@link JarFile}, null if Jar cannot be created from {@link File} passed in. + */ + protected JarFile processFile(final File file) { + if (file.isDirectory() || !file.getName().toLowerCase().contains(PreprocessConstants.JAR_EXTENSION)) return null; + + final JarFile jar = makeJarFile(file); + + if (jar != null) { + injectFileToSystemClassPath(file); + } + return jar; + } + + /** + * Creates a {@link JarFile} from {@link File}. + * + * @param file file to construct the Jar file from + * @return a valid {@link JarFile}, null if invalid + */ + protected JarFile makeJarFile(final File file) { + try { + return new JarFile(file); + } catch (IOException e) { + log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to create JarFile from file: " + file.getName(), e); + return null; + } + } + + /** + * Add the file to the system class path using the {@link Injector injector} api. + * + * @param file Jar containing a set of classes to be added to the class path + */ + protected void injectFileToSystemClassPath(final File file) { + log.debug(PreprocessConstants.MESSAGE_PREFIX + "Injecting file to system class path: " + file.getName()); + Injector.addToSystemClasspath(Injector.createInstrumentation(), file); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java new file mode 100644 index 0000000..999e9c4 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.modules; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; + +import java.io.File; +import java.util.List; +import java.util.jar.JarFile; + +/** + * Class that holds data of a Jar package that has been loaded by a {@link JarModuleLoader} including the export strategy + * that will be used to store the transformed classes. + */ +@AllArgsConstructor +@Getter +public class ModuleInfo { + private final File file; + private final JarFile jarFile; + private final List classNames; + private final ModuleExportStrategy exportStrategy; +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java new file mode 100644 index 0000000..947674b --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.modules; + +import java.util.List; + +/** + * Interface for ModuleLoaders that load modules to be instrumented + */ +public interface ModuleLoader { + /** + * Loads all the modules found under the paths specified and aggregate them into a single list of {@link ModuleInfo}. + * Names of all the classes within each package are discovered and stored inside {@link ModuleInfo}. + * + * @return list of {@link ModuleInfo} loaded by this package loader. Empty if no modules found. + */ + List loadPackages(); +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/PreprocessConstants.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/PreprocessConstants.java new file mode 100644 index 0000000..237d70b --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/PreprocessConstants.java @@ -0,0 +1,9 @@ +package software.amazon.disco.instrumentation.preprocess.util; + +/** + * Holds constant variables used throughout the library + */ +public class PreprocessConstants { + public static final String MESSAGE_PREFIX = "Disco(Instrumentation preprocess) - "; + public static final String JAR_EXTENSION = ".jar"; +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java new file mode 100644 index 0000000..fd6996c --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.cli; + +import org.apache.logging.log4j.Level; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Arrays; + +public class PreprocessConfigParserTest { + String outputDir = "/d"; + String serialization = "/s"; + String agent = "/a"; + String suffix = "-suffix"; + + static PreprocessConfigParser preprocessConfigParser; + + @Before + public void before() { + preprocessConfigParser = new PreprocessConfigParser(); + } + + @Test + public void parseCommandLineReturnsNullWithNullArgs() { + Assert.assertNull(preprocessConfigParser.parseCommandLine(null)); + } + + @Test + public void parseCommandLineReturnsNullWithEmptyArgs() { + Assert.assertNull(preprocessConfigParser.parseCommandLine(new String[]{})); + } + + @Test + public void parseCommandLineReturnsNullWithInvalidFlag() { + String[] args = new String[]{"--help", "--suff", suffix}; + Assert.assertNull(preprocessConfigParser.parseCommandLine(args)); + } + + @Test + public void parseCommandLineReturnsNullWithUnmatchedFlagAsLastArg() { + String[] args = new String[]{"--help", "--suffix"}; + Assert.assertNull(preprocessConfigParser.parseCommandLine(args)); + } + + @Test + public void parseCommandLineReturnsNullWithHelpFlag() { + String[] args = new String[]{"--help", "--suffix", suffix}; + Assert.assertNull(preprocessConfigParser.parseCommandLine(args)); + } + + @Test + public void parseCommandLineReturnsNullWithInvalidFormat() { + String[] args = new String[]{"--suffix", "--verbose"}; + Assert.assertNull(preprocessConfigParser.parseCommandLine(args)); + } + + @Test + public void parseCommandLineWorksWithDifferentLogLevels() { + PreprocessConfig config = preprocessConfigParser.parseCommandLine(new String[]{"--verbose"}); + PreprocessConfig silentConfig = preprocessConfigParser.parseCommandLine(new String[]{"--silent"}); + + Assert.assertEquals(Level.TRACE, config.getLogLevel()); + Assert.assertEquals(Level.OFF, silentConfig.getLogLevel()); + } + + @Test + public void parseCommandLineWorksWithFullCommandNamesAndReturnsConfigFile() { + String[] args = new String[]{ + "--outputDir", outputDir, + "--jarpaths", "/d1", "/d2", "/d3", + "--serializationpath", serialization, + "--agentPath", agent, + "--suffix", suffix + }; + + PreprocessConfig config = preprocessConfigParser.parseCommandLine(args); + + Assert.assertEquals(outputDir, config.getOutputDir()); + Assert.assertEquals(serialization, config.getSerializationJarPath()); + Assert.assertEquals(Arrays.asList("/d1", "/d2", "/d3"), config.getJarPaths()); + Assert.assertEquals(agent, config.getAgentPath()); + Assert.assertEquals(suffix, config.getSuffix()); + } + + @Test + public void parseCommandLineWorksWithShortHandCommandNamesAndReturnsConfigFile() { + String[] args = new String[]{ + "-out", outputDir, + "-jps", "/d1", "/d2", "/d3", + "-sp", serialization, + "-ap", agent, + "-suf", suffix + }; + + PreprocessConfig config = preprocessConfigParser.parseCommandLine(args); + + Assert.assertEquals(outputDir, config.getOutputDir()); + Assert.assertEquals(serialization, config.getSerializationJarPath()); + Assert.assertEquals(Arrays.asList("/d1", "/d2", "/d3"), config.getJarPaths()); + Assert.assertEquals(agent, config.getAgentPath()); + Assert.assertEquals(suffix, config.getSuffix()); + } + + @Test + public void parseCommandLineWorkWithHelpFlag() { + PreprocessConfigParser spyParser = Mockito.spy(preprocessConfigParser); + + spyParser.parseCommandLine(new String[]{"--help"}); + Mockito.verify(spyParser).printHelpText(); + Mockito.clearInvocations(spyParser); + + spyParser.parseCommandLine(new String[]{"--verbose", "--help"}); + Mockito.verify(spyParser, Mockito.never()).printHelpText(); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java new file mode 100644 index 0000000..d8a0f60 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java @@ -0,0 +1,267 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.export; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleExportException; +import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; +import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; +import software.amazon.disco.instrumentation.preprocess.util.MockEntities; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; + +@RunWith(MockitoJUnitRunner.class) +public class JarModuleExportStrategyTest { + static final String ORIGINAL_FILE_PATH = System.getProperty("java.io.tmpdir") + "disco/tests/" + "original.jar"; + static final String DESTINATION_PATH = System.getProperty("java.io.tmpdir") + "disco/tests/destination"; + static final String PACKAGE_SUFFIX = "suffix"; + + ModuleInfo moduleInfo; + + @Mock + Map instrumented; + + @Mock + JarFile mockJarFile; + + @Mock + File mockFile; + + @Mock + JarModuleExportStrategy mockStrategy; + + @Mock + JarOutputStream mockJarOS; + + JarModuleExportStrategy strategy; + + @Before + public void before() { + strategy = new JarModuleExportStrategy(); + moduleInfo = MockEntities.makeMockPackageInfo(); + Mockito.doCallRealMethod().when(mockStrategy).export(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + } + + @Test + public void testExportWorksAndInvokesCreateTempFile() { + mockStrategy.export(moduleInfo, instrumented, null, null); + Mockito.verify(mockStrategy).createTempFile(moduleInfo); + } + + @Test + public void testExportWorksAndInvokesBuildOutputJar() { + mockStrategy.export(moduleInfo, instrumented, null, null); + Mockito.verify(mockStrategy).buildOutputJar(Mockito.any(), Mockito.any(), Mockito.any()); + } + + @Test + public void testExportWorksAndInvokesMoveTempFileToDestination() { + mockStrategy.export(moduleInfo, instrumented, null, null); + Mockito.verify(mockStrategy).moveTempFileToDestination(Mockito.eq(moduleInfo), Mockito.any(), Mockito.any()); + } + + @Test + public void testSaveTransformedClassesWorksAndCreatesNewEntries() throws IOException { + strategy.saveTransformedClasses(mockJarOS, MockEntities.makeInstrumentedClassesMap()); + + ArgumentCaptor jarEntryArgument = ArgumentCaptor.forClass(JarEntry.class); + Mockito.verify(mockJarOS, Mockito.times(3)).putNextEntry(jarEntryArgument.capture()); + Assert.assertTrue(jarEntryArgument.getAllValues().size() == 3); + + ArgumentCaptor byteArrayArgument = ArgumentCaptor.forClass(byte[].class); + Mockito.verify(mockJarOS, Mockito.times(3)).write(byteArrayArgument.capture()); + Assert.assertTrue(byteArrayArgument.getAllValues().size() == 3); + + Mockito.verify(mockJarOS, Mockito.times(3)).closeEntry(); + } + + @Test + public void testCopyExistingJarEntriesWorksWithFilesAndPath() throws IOException { + Mockito.doCallRealMethod().when(mockStrategy).copyExistingJarEntries(Mockito.eq(mockJarOS), Mockito.any(), Mockito.any()); + + mockStrategy.copyExistingJarEntries(mockJarOS, moduleInfo, MockEntities.makeInstrumentedClassesMap()); + + // 3 out of 6 classes have not been instrumented + Mockito.verify(mockStrategy, Mockito.times(3)).copyJarEntry(Mockito.eq(mockJarOS), Mockito.any(), Mockito.any()); + + // 1 path to be copied + ArgumentCaptor jarEntryArgument = ArgumentCaptor.forClass(JarEntry.class); + Mockito.verify(mockJarOS).putNextEntry(jarEntryArgument.capture()); + Assert.assertTrue(jarEntryArgument.getValue().getName().equals("pathA/")); + } + + @Test(expected = UnableToReadJarEntryException.class) + public void testCopyJarEntryFailsWithNullEntry() throws IOException { + Mockito.doReturn(null).when(mockJarFile).getInputStream(Mockito.any()); + + strategy.copyJarEntry(mockJarOS, mockJarFile, new JarEntry("fff")); + } + + @Test + public void testCopyJarEntryWorksAndWritesToOS() throws IOException { + InputStream mockStream = Mockito.mock(InputStream.class); + + Mockito.when(mockJarFile.getInputStream(null)).thenReturn(mockStream); + Mockito.when(mockStream.read(Mockito.any())).thenReturn(1).thenReturn(-1); + + strategy.copyJarEntry(mockJarOS, mockJarFile, null); + + Mockito.verify(mockJarOS).putNextEntry(null); + Mockito.verify(mockJarOS).write(Mockito.any(), Mockito.eq(0), Mockito.eq(1)); + Mockito.verify(mockJarOS).closeEntry(); + } + + @Test + public void testCreateTempFileWorks() { + File file = strategy.createTempFile(moduleInfo); + + Assert.assertNotNull(file); + file.delete(); + } + + @Test + public void testMoveTempFileToDestinationToReplaceOriginal() throws IOException { + // create original file and assume temp/disco is where the original package is + File tempFile = createOriginalFile(); + + long originalLength = tempFile.length(); + + // create temp file named path.temp.jar + Mockito.when(moduleInfo.getFile()).thenReturn(mockFile); + Mockito.when(mockFile.getAbsolutePath()).thenReturn(ORIGINAL_FILE_PATH); + File file = strategy.createTempFile(moduleInfo); + + // replace original file + Path path = strategy.moveTempFileToDestination(moduleInfo, null, file); + + Assert.assertNotEquals(originalLength, path.toFile().length()); + Assert.assertEquals(tempFile.getAbsolutePath(), path.toFile().getAbsolutePath()); + + file.delete(); + path.toFile().delete(); + } + + @Test + public void testMoveTempFileToDestinationWorks() throws IOException { + String destination = DESTINATION_PATH; + strategy = new JarModuleExportStrategy(destination); + + // create original file and assume temp/disco/tests is where the original package is + File original = createOriginalFile(); + + File file = strategy.createTempFile(moduleInfo); + + // move to destination + Path path = strategy.moveTempFileToDestination(moduleInfo, null, file); + + Assert.assertEquals(destination, path.toFile().getParentFile().getAbsolutePath()); + Assert.assertEquals(moduleInfo.getFile().getName(), path.toFile().getName()); + Assert.assertTrue(original.exists()); + + file.delete(); + path.toFile().delete(); + original.delete(); + } + + @Test + public void testMoveTempFileToDestinationWorksWithSuffix() throws IOException { + String destination = DESTINATION_PATH; + strategy = new JarModuleExportStrategy(destination); + + // create original file and assume temp/disco/tests is where the original package is + File original = createOriginalFile(); + + File file = strategy.createTempFile(moduleInfo); + + // move to destination + Path path = strategy.moveTempFileToDestination(moduleInfo, PACKAGE_SUFFIX, file); + + String nameToCheck = moduleInfo.getFile() + .getName() + .substring(0, moduleInfo.getFile().getName().lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + + PACKAGE_SUFFIX + + PreprocessConstants.JAR_EXTENSION; + + Assert.assertEquals(destination, path.toFile().getParentFile().getAbsolutePath()); + Assert.assertEquals(nameToCheck, path.toFile().getName()); + Assert.assertTrue(original.exists()); + + file.delete(); + path.toFile().delete(); + original.delete(); + } + + @Test(expected = ModuleExportException.class) + public void testMoveTempFileToDestinationFailsWhenFileAlreadyExistsWithSameName() throws IOException { + String destination = DESTINATION_PATH; + strategy = new JarModuleExportStrategy(destination); + + // create original file and assume temp/disco/tests is where the original package is + File original = createOriginalFile(); + + File file = strategy.createTempFile(moduleInfo); + + // move to destination + Path path = strategy.moveTempFileToDestination(moduleInfo, PACKAGE_SUFFIX, file); + + String nameToCheck = moduleInfo.getFile() + .getName() + .substring(0, moduleInfo.getFile().getName().lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + + PACKAGE_SUFFIX + + PreprocessConstants.JAR_EXTENSION; + + Assert.assertEquals(destination, path.toFile().getParentFile().getAbsolutePath()); + Assert.assertEquals(nameToCheck, path.toFile().getName()); + Assert.assertTrue(original.exists()); + + // mock another package of the same name being moved to the same dir as the first one + try { + strategy.moveTempFileToDestination(moduleInfo, PACKAGE_SUFFIX, file); + } finally { + file.delete(); + path.toFile().delete(); + original.delete(); + } + } + + private File createOriginalFile() throws IOException { + File tempFile = new File(ORIGINAL_FILE_PATH); + + tempFile.getParentFile().mkdirs(); + FileOutputStream os = new FileOutputStream(tempFile); + os.write(12); + os.close(); + + return tempFile; + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/InstrumentedClassStateTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/InstrumentedClassStateTest.java new file mode 100644 index 0000000..38524fe --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/InstrumentedClassStateTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.instrumentation; + +import org.junit.Assert; +import org.junit.Test; + +public class InstrumentedClassStateTest { + private static final String INSTALLABLE_ID_1 = "id_1"; + private static final String INSTALLABLE_ID_2 = "id_2"; + private static final byte[] INITIAL= new byte[]{123}; + private static final byte[] UPDATED= new byte[]{45}; + + @Test + public void testConstructorWorks(){ + InstrumentedClassState data = new InstrumentedClassState(INSTALLABLE_ID_1, INITIAL); + + Assert.assertTrue(data.getClassBytes().equals(INITIAL)); + Assert.assertTrue(data.getInstallableIds().contains(INSTALLABLE_ID_1)); + } + + @Test + public void testUpdateWorks(){ + InstrumentedClassState data = new InstrumentedClassState(INSTALLABLE_ID_1, INITIAL); + data.update(INSTALLABLE_ID_2, UPDATED); + + Assert.assertTrue(data.getClassBytes().equals(UPDATED)); + Assert.assertTrue(data.getInstallableIds().contains(INSTALLABLE_ID_1)); + Assert.assertTrue(data.getInstallableIds().contains(INSTALLABLE_ID_2)); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java new file mode 100644 index 0000000..22ce423 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.instrumentation; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.disco.instrumentation.preprocess.exceptions.AgentLoaderNotProvidedException; +import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleLoaderNotProvidedException; +import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.loaders.agents.DiscoAgentLoader; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.JarModuleLoader; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; +import software.amazon.disco.instrumentation.preprocess.util.MockEntities; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class ModuleTransformerTest { + private static final String PACKAGE_SUFFIX = "suffix"; + + ModuleTransformer spyTransformer; + + @Mock + DiscoAgentLoader mockAgentLoader; + + @Mock + JarModuleLoader mockJarPackageLoader; + + List moduleInfos; + + @Before + public void before() { + spyTransformer = Mockito.spy( + ModuleTransformer.builder() + .jarLoader(mockJarPackageLoader) + .agentLoader(mockAgentLoader) + .suffix(PACKAGE_SUFFIX) + .build() + ); + + Mockito.doReturn(Arrays.asList(MockEntities.makeMockPackageInfo())).when(mockJarPackageLoader).loadPackages(); + + moduleInfos = new ArrayList<>(); + moduleInfos.add(Mockito.mock(ModuleInfo.class)); + moduleInfos.add(Mockito.mock(ModuleInfo.class)); + } + + @Test + public void testTransformWorksWithDefaultLogLevel(){ + spyTransformer.transform(); + + Assert.assertEquals(LogManager.getLogger().getLevel(), Level.INFO); + } + + @Test + public void testTransformWorksWithVerboseLogLevel(){ + spyTransformer = Mockito.spy( + ModuleTransformer.builder() + .jarLoader(mockJarPackageLoader) + .agentLoader(mockAgentLoader) + .logLevel(Level.TRACE) + .build() + ); + + spyTransformer.transform(); + + Assert.assertEquals(Level.TRACE, LogManager.getLogger().getLevel()); + } + + @Test(expected = AgentLoaderNotProvidedException.class) + public void testTransformFailsWhenNoAgentLoaderProvided(){ + spyTransformer = Mockito.spy( + ModuleTransformer.builder() + .jarLoader(mockJarPackageLoader) + .build() + ); + spyTransformer.transform(); + } + + + @Test(expected = ModuleLoaderNotProvidedException.class) + public void testTransformFailsWhenNoPackageLoaderProvided() { + spyTransformer = Mockito.spy( + ModuleTransformer.builder() + .agentLoader(mockAgentLoader) + .build() + ); + spyTransformer.transform(); + } + + @Test + public void testTransformWorksAndInvokesLoadAgentAndPackages() { + spyTransformer = Mockito.spy( + ModuleTransformer.builder() + .jarLoader(mockJarPackageLoader) + .agentLoader(mockAgentLoader) + .build() + ); + spyTransformer.transform(); + + Mockito.verify(mockAgentLoader).loadAgent(); + Mockito.verify(mockJarPackageLoader).loadPackages(); + } + + @Test + public void testTransformWorksAndInvokesPackageLoader() { + spyTransformer.transform(); + + Mockito.verify(mockJarPackageLoader).loadPackages(); + Mockito.verify(spyTransformer).applyInstrumentation(Mockito.any()); + } + + + @Test + public void testApplyInstrumentationWorksAndInvokesExport() { + Mockito.doCallRealMethod().when(spyTransformer).applyInstrumentation(Mockito.any()); + + JarModuleExportStrategy s1 = Mockito.mock(JarModuleExportStrategy.class); + Mockito.when(moduleInfos.get(0).getExportStrategy()).thenReturn(s1); + + Map instrumentedClasses = MockEntities.makeInstrumentedClassesMap(); + Mockito.doReturn(instrumentedClasses).when(spyTransformer).getInstrumentedClasses(); + + spyTransformer.applyInstrumentation(moduleInfos.get(0)); + + Mockito.verify(moduleInfos.get(0)).getClassNames(); + Mockito.verify(s1).export(moduleInfos.get(0), instrumentedClasses, null, PACKAGE_SUFFIX); + Assert.assertTrue(instrumentedClasses.isEmpty()); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java new file mode 100644 index 0000000..b05462e --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java @@ -0,0 +1,62 @@ +package software.amazon.disco.instrumentation.preprocess.instrumentation; + +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.disco.instrumentation.preprocess.util.MockEntities; + +@RunWith(MockitoJUnitRunner.class) +public class TransformationListenerTest { + static DynamicType mockDynamicTypeWithAuxiliary; + static TransformationListener mockListener; + static String uid = "112233"; + + @Mock + TypeDescription mockTypeDescription; + + @Before + public void before() { + mockListener = Mockito.spy(new TransformationListener(uid)); + mockDynamicTypeWithAuxiliary = MockEntities.makeMockDynamicTypeWithAuxiliaryClasses(); + + Mockito.when(mockTypeDescription.getInternalName()).thenReturn(MockEntities.makeClassPaths().get(0)); + } + + @Test + public void testOnTransformationWorksAndInvokesCollectDataFromEvent() { + Mockito.doCallRealMethod().when(mockListener).onTransformation(mockTypeDescription, null, null, false, mockDynamicTypeWithAuxiliary); + + mockListener.onTransformation(mockTypeDescription, null, null, false, mockDynamicTypeWithAuxiliary); + + Mockito.verify(mockListener).collectDataFromEvent(mockTypeDescription, mockDynamicTypeWithAuxiliary); + } + + @Test + public void testCollectDataFromEventWorksAndPopulatesMapWithNewEntries() { + TransformationListener mockListener = Mockito.spy(new TransformationListener(uid)); + Mockito.doCallRealMethod().when(mockListener).collectDataFromEvent(mockTypeDescription, mockDynamicTypeWithAuxiliary); + + mockListener.collectDataFromEvent(mockTypeDescription, mockDynamicTypeWithAuxiliary); + + Assert.assertTrue(TransformationListener.getInstrumentedTypes().size() == 3); + } + + @Test + public void testCollectDataFromEventWorksAndUpdatesExistingEntriesFromMap() { + DynamicType updatedType = MockEntities.makeMockDynamicType(); + + Mockito.doCallRealMethod().when(mockListener).collectDataFromEvent(mockTypeDescription, mockDynamicTypeWithAuxiliary); + Mockito.doCallRealMethod().when(mockListener).collectDataFromEvent(mockTypeDescription, updatedType); + + mockListener.collectDataFromEvent(mockTypeDescription, mockDynamicTypeWithAuxiliary); + mockListener.collectDataFromEvent(mockTypeDescription, updatedType); + + Assert.assertTrue(TransformationListener.getInstrumentedTypes().size() == 3); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java new file mode 100644 index 0000000..ce6023c --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.agents; + +import org.junit.Test; +import software.amazon.disco.instrumentation.preprocess.exceptions.NoPathProvidedException; + +public class DiscoAgentLoaderTest { + @Test(expected = NoPathProvidedException.class) + public void testConstructorFailOnNullPaths() throws NoPathProvidedException { + new DiscoAgentLoader(null); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java new file mode 100644 index 0000000..b6bc602 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java @@ -0,0 +1,201 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.modules; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; +import software.amazon.disco.instrumentation.preprocess.exceptions.NoPathProvidedException; +import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.util.MockEntities; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +@RunWith(MockitoJUnitRunner.class) +public class JarModuleLoaderTest { + static final List PATHS = MockEntities.makeMockPathsWithDuplicates(); + static final List MOCK_FILES = MockEntities.makeMockFiles(); + static final List MOCK_JAR_ENTRIES = MockEntities.makeMockJarEntries(); + + JarModuleLoader loader; + + @Mock + JarFile jarFile; + + @Mock + File mockFile; + + @Before + public void before() throws NoPathProvidedException { + loader = new JarModuleLoader(PATHS); + + Mockito.when(mockFile.isDirectory()).thenReturn(false); + Mockito.when(mockFile.getName()).thenReturn("ATestJar.jar"); + } + + @Test(expected = NoPathProvidedException.class) + public void testConstructorFailWithEmptyPathList() throws NoPathProvidedException { + new JarModuleLoader(new ArrayList<>()); + } + + @Test(expected = NoPathProvidedException.class) + public void testConstructorFailWithNullPathList() throws NoPathProvidedException { + new JarModuleLoader(null); + } + + @Test + public void testConstructorWorksAndHasDefaultStrategy() { + Assert.assertTrue(loader.getStrategy().getClass().equals(JarModuleExportStrategy.class)); + } + + @Test + public void testConstructorWorksAndNoDuplicatePaths() { + Assert.assertTrue(loader.getPaths().size() == PATHS.size() - 1); + + for (String path : PATHS) { + Assert.assertTrue(loader.getPaths().contains(path)); + } + } + + @Test + public void testConstructorWorksWithNonDefaultStrategy() { + ModuleExportStrategy mockStrategy = Mockito.mock(ModuleExportStrategy.class); + + loader = new JarModuleLoader(mockStrategy, PATHS); + Assert.assertNotEquals(JarModuleExportStrategy.class, loader.getStrategy().getClass()); + } + + @Test + public void testProcessFileWorksWithValidFileExtension(){ + JarModuleLoader loader = Mockito.mock(JarModuleLoader.class); + JarFile jar = Mockito.mock(JarFile.class); + + Mockito.doCallRealMethod().when(loader).processFile(mockFile); + Mockito.doReturn(jar).when(loader).makeJarFile(mockFile); + + Assert.assertNotNull(loader.processFile(mockFile)); + + Mockito.when(mockFile.getName()).thenReturn("ATestJar.JAR"); + Assert.assertNotNull(loader.processFile(mockFile)); + } + + @Test + public void testProcessFileWorksWithInvalidFileExtensionAndReturnNull(){ + JarModuleLoader loader = Mockito.mock(JarModuleLoader.class); + + Mockito.when(mockFile.getName()).thenReturn("ATestJar.txt"); + Mockito.doCallRealMethod().when(loader).processFile(mockFile); + + Assert.assertNull(loader.processFile(mockFile)); + } + + @Test + public void testProcessFileWorksAndInvokesInjectFileToSystemClassPath() { + JarModuleLoader packageLoader = Mockito.mock(JarModuleLoader.class); + Mockito.when(packageLoader.processFile(Mockito.any())).thenCallRealMethod(); + + JarFile mockJarfile = Mockito.mock(JarFile.class); + Mockito.when(packageLoader.makeJarFile(mockFile)).thenReturn(mockJarfile); + + packageLoader.processFile(mockFile); + + Mockito.verify(packageLoader).injectFileToSystemClassPath(mockFile); + } + + @Test(expected = NoModuleToInstrumentException.class) + public void testLoadPackagesFailOnEmptyPackageInfoList() { + loader.loadPackages(); + } + + @Test + public void testLoadPackagesWorksWithOnePackageInfo() { + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(Arrays.asList(PATHS.get(0)))); + + Mockito.doCallRealMethod().when(packageLoader).loadPackages(); + Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(0))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); + Mockito.doReturn(MockEntities.makeMockPackageInfo()).when(packageLoader).loadPackage(MOCK_FILES.get(0)); + + packageLoader.loadPackages(); + + Mockito.verify(packageLoader).loadPackage(Mockito.any()); + } + + @Test(expected = NoModuleToInstrumentException.class) + public void testLoadPackagesFailsWithNoPackageInfoCreated() { + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(Arrays.asList(PATHS.get(0)))); + + Mockito.doCallRealMethod().when(packageLoader).loadPackages(); + Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(0))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); + + packageLoader.loadPackages(); + + Mockito.verify(packageLoader).loadPackage(Mockito.any()); + } + + @Test + public void testLoadPackagesWorksAndCalledThreeTimesWithThreePaths() { + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(PATHS)); + + Mockito.doCallRealMethod().when(packageLoader).loadPackages(); + + Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(1))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); + Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(2))).thenReturn(Arrays.asList(MOCK_FILES.get(1))); + Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(3))).thenReturn(Arrays.asList(MOCK_FILES.get(2))); + + try { + packageLoader.loadPackages(); + } catch (NoModuleToInstrumentException e) { + // swallow + } + + Mockito.verify(packageLoader, Mockito.times(3)).loadPackage(Mockito.any()); + } + + @Test + public void testLoadPackagesWorksAndReturnsValidPackageInfoObjectAndInvokesProcessFile() { + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(new JarModuleExportStrategy(), PATHS)); + + List classes = MOCK_JAR_ENTRIES + .stream() + .map(jarEntry -> jarEntry.getName().substring(0, jarEntry.getName().lastIndexOf(".class"))) + .collect(Collectors.toList()); + + Mockito.doCallRealMethod().when(packageLoader).loadPackage(MOCK_FILES.get(0)); + Mockito.doReturn(jarFile).when(packageLoader).processFile(MOCK_FILES.get(0)); + Mockito.doReturn(MOCK_JAR_ENTRIES).when(packageLoader).extractEntries(Mockito.any()); + + final ModuleInfo info = packageLoader.loadPackage(MOCK_FILES.get(0)); + + Mockito.verify(packageLoader, Mockito.times(1)).processFile(Mockito.any()); + Assert.assertTrue(info.getClassNames().size() == MOCK_JAR_ENTRIES.size()); + Assert.assertArrayEquals(classes.toArray(), info.getClassNames().toArray()); + Assert.assertSame(MOCK_FILES.get(0), info.getFile()); + Assert.assertSame(jarFile, info.getJarFile()); + Assert.assertSame(JarModuleExportStrategy.class, info.getExportStrategy().getClass()); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java new file mode 100644 index 0000000..18cca97 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java @@ -0,0 +1,134 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.util; + +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import org.mockito.Mockito; +import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; +import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * A class that holds mock entities for testing purposes + */ +public class MockEntities { + public static List makeClassPaths(){ + return Arrays.asList("com/test/folder/ClassA", "com/test/folder/ClassB", "com/test/folder/ClassC"); + } + + public static List makeMockJarEntriesWithPath() { + final List list = makeMockJarEntries(); + + list.add(new JarEntry("ClassD.class")); + list.add(new JarEntry("ClassE.class")); + list.add(new JarEntry("ClassF.class")); + list.add(new JarEntry("pathA/")); + + return list; + } + + public static List makeMockJarEntries() { + final List list = new ArrayList<>(); + + list.add(new JarEntry("ClassA.class")); + list.add(new JarEntry("ClassB.class")); + list.add(new JarEntry("ClassC.class")); + + return list; + } + + public static Map makeInstrumentedClassesMap() { + final Map classes = new HashMap<>(); + final InstrumentedClassState stateOne = new InstrumentedClassState("installable_a", new byte[]{12}); + + stateOne.update("installable_b", new byte[]{15}); + classes.put("ClassD", stateOne); + classes.put("ClassE", new InstrumentedClassState("installable_b", new byte[]{13})); + classes.put("ClassF", new InstrumentedClassState("installable_c", new byte[]{14})); + + return classes; + } + + public static List makeMockFiles() { + return Arrays.asList(new File("file_a"), + new File("file_b"), + new File("file_c")); + } + + + public static List makeMockPathsWithDuplicates() { + return Arrays.asList("path_a", "path_a", "path_b", "path_c"); + } + + public static DynamicType makeMockDynamicTypeWithAuxiliaryClasses(){ + final DynamicType type = Mockito.mock(DynamicType.class); + + final TypeDescription auxiliary_1 = Mockito.mock(TypeDescription.class); + final TypeDescription auxiliary_2 = Mockito.mock(TypeDescription.class); + + Map auxiliaryTypesMap = new HashMap<>(); + Mockito.doReturn(auxiliaryTypesMap).when(type).getAuxiliaryTypes(); + Mockito.doReturn(new byte[]{02}).when(type).getBytes(); + Mockito.doReturn("internal_1$auxiliary123").when(auxiliary_1).getInternalName(); + Mockito.doReturn("internal_2$auxiliary123").when(auxiliary_2).getInternalName(); + + auxiliaryTypesMap.put(auxiliary_1, new byte[]{00}); + auxiliaryTypesMap.put(auxiliary_2, new byte[]{01}); + + return type; + } + + public static DynamicType makeMockDynamicType(){ + final DynamicType type = Mockito.mock(DynamicType.class); + + Map auxiliaryTypesMap = new HashMap<>(); + Mockito.doReturn(auxiliaryTypesMap).when(type).getAuxiliaryTypes(); + Mockito.doReturn(new byte[]{9}).when(type).getBytes(); + + return type; + } + + public static ModuleInfo makeMockPackageInfo(){ + final ModuleInfo info = Mockito.mock(ModuleInfo.class); + + final File mockFile = Mockito.mock(File.class); + final JarFile mockJarFile = Mockito.mock(JarFile.class); + final ModuleExportStrategy mockStrategy = Mockito.mock(ModuleExportStrategy.class); + + Mockito.lenient().when(info.getFile()).thenReturn(mockFile); + Mockito.lenient().when(info.getJarFile()).thenReturn(mockJarFile); + Mockito.lenient().when(mockJarFile.getName()).thenReturn("mock.jar"); + Mockito.lenient().when(info.getExportStrategy()).thenReturn(mockStrategy); + Mockito.lenient().when(mockFile.getName()).thenReturn("mock.jar"); + + final Enumeration entries = Collections.enumeration(makeMockJarEntriesWithPath()); + Mockito.lenient().when(mockJarFile.entries()).thenReturn(entries); + + return info; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 02650f0..06f0fea 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,4 +29,5 @@ include("disco-java-agent-sql:disco-java-agent-sql-plugin") include("disco-java-agent-example") include("disco-java-agent-example-test") -include("disco-java-agent-example-injector-test") \ No newline at end of file +include("disco-java-agent-example-injector-test") +include("disco-java-agent-instrumentation-preprocess") From c82b7f827b0497b3b52dadfe165cc4465b58e024 Mon Sep 17 00:00:00 2001 From: Hongbo Liu Date: Thu, 16 Jul 2020 21:37:12 -0400 Subject: [PATCH 25/45] fix instrumentation preprocessing library not working on certain linux hosts --- .../preprocess/cli/PreprocessConfig.java | 3 +- .../export/JarModuleExportStrategy.java | 16 +- .../export/ModuleExportStrategy.java | 4 +- .../instrumentation/ModuleTransformer.java | 4 +- .../export/JarModuleExportStrategyTest.java | 174 +++++++----------- .../ModuleTransformerTest.java | 2 +- 6 files changed, 74 insertions(+), 129 deletions(-) diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java index 42a2f7f..2eab084 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java @@ -28,9 +28,10 @@ @Builder @Getter public class PreprocessConfig { - private final String outputDir; @Singular private final List jarPaths; + + private final String outputDir; private final String agentPath; private final String suffix; private final Level logLevel; diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java index b041f7e..b5218af 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java @@ -21,7 +21,6 @@ import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; -import software.amazon.disco.instrumentation.preprocess.serialization.InstrumentationState; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; import java.io.File; @@ -68,13 +67,12 @@ public JarModuleExportStrategy() { * Exports all transformed classes to a Jar file. A temporary Jar File will be created to store all * the transformed classes and then be renamed to replace the original Jar. * - * @param moduleInfo Information of the original Jar - * @param instrumented a map of instrumented classes with their bytecode - * @param instrumentationState state to be passed to the runtime agent to dynamically skip already instrumented classes - * @param suffix suffix of the transformed package + * @param moduleInfo Information of the original Jar + * @param instrumented a map of instrumented classes with their bytecode + * @param suffix suffix of the transformed package */ @Override - public void export(final ModuleInfo moduleInfo, final Map instrumented, final InstrumentationState instrumentationState, final String suffix) { + public void export(final ModuleInfo moduleInfo, final Map instrumented, final String suffix) { log.debug(PreprocessConstants.MESSAGE_PREFIX + "Saving changes to Jar"); final File file = createTempFile(moduleInfo); @@ -223,10 +221,8 @@ protected Path moveTempFileToDestination(final ModuleInfo moduleInfo, final Stri final Path destination = Paths.get(destinationStrWithSuffix); destination.toFile().getParentFile().mkdirs(); - final boolean isOverride = outputDir == null && suffix == null; - - if (!isOverride && destination.toFile().exists()) { - throw new ModuleExportException("Failed move transformed package to output directory, package with same name already present: " + destinationStrWithSuffix, null); + if (moduleInfo.getFile().getAbsolutePath().equals(destination.toFile().getAbsolutePath())) { + log.info(PreprocessConstants.MESSAGE_PREFIX + "Overriding original file: " + moduleInfo.getFile().getName()); } final Path filePath = Files.move( diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java index 0bd309e..b2e0b78 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java @@ -17,7 +17,6 @@ import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; -import software.amazon.disco.instrumentation.preprocess.serialization.InstrumentationState; import java.util.Map; @@ -30,8 +29,7 @@ public interface ModuleExportStrategy { * * @param info Information of the original Jar * @param instrumented a map of instrumented classes with their bytecode - * @param instrumentationState state to be passed to the runtime agent to dynamically skip already instrumented classes * @param suffix suffix to be appended to the transformed package */ - void export(final ModuleInfo info, final Map instrumented, final InstrumentationState instrumentationState, final String suffix); + void export(final ModuleInfo info, final Map instrumented, final String suffix); } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java index f093b46..3617f01 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java @@ -27,7 +27,6 @@ import software.amazon.disco.instrumentation.preprocess.loaders.agents.AgentLoader; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleLoader; -import software.amazon.disco.instrumentation.preprocess.serialization.InstrumentationState; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; import java.util.Map; @@ -46,7 +45,6 @@ public class ModuleTransformer { private final ModuleLoader jarLoader; private final AgentLoader agentLoader; - private final InstrumentationState instrumentationState; private final String suffix; private final Level logLevel; @@ -91,7 +89,7 @@ protected void applyInstrumentation(final ModuleInfo moduleInfo) { } } - moduleInfo.getExportStrategy().export(moduleInfo, getInstrumentedClasses(), instrumentationState, suffix); + moduleInfo.getExportStrategy().export(moduleInfo, getInstrumentedClasses(), suffix); // empty the map in preparation for transforming another package getInstrumentedClasses().clear(); diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java index d8a0f60..b6c2485 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java @@ -17,12 +17,11 @@ import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleExportException; import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; @@ -40,59 +39,56 @@ import java.util.jar.JarFile; import java.util.jar.JarOutputStream; -@RunWith(MockitoJUnitRunner.class) public class JarModuleExportStrategyTest { - static final String ORIGINAL_FILE_PATH = System.getProperty("java.io.tmpdir") + "disco/tests/" + "original.jar"; - static final String DESTINATION_PATH = System.getProperty("java.io.tmpdir") + "disco/tests/destination"; static final String PACKAGE_SUFFIX = "suffix"; + static final String TEMP_FILE_NAME = "temp.jar"; + static final String ORIGINAL_FILE_NAME = "mock.jar"; + static final String OUT_DIR = "out"; - ModuleInfo moduleInfo; + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); - @Mock + ModuleInfo mockModuleInfo; Map instrumented; - - @Mock JarFile mockJarFile; - - @Mock - File mockFile; - - @Mock - JarModuleExportStrategy mockStrategy; - - @Mock JarOutputStream mockJarOS; - - JarModuleExportStrategy strategy; + JarModuleExportStrategy mockStrategy; + JarModuleExportStrategy spyStrategy; @Before - public void before() { - strategy = new JarModuleExportStrategy(); - moduleInfo = MockEntities.makeMockPackageInfo(); - Mockito.doCallRealMethod().when(mockStrategy).export(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + public void before() throws IOException { + instrumented = Mockito.mock(Map.class); + mockJarFile = Mockito.mock(JarFile.class); + mockStrategy = Mockito.mock(JarModuleExportStrategy.class); + mockJarOS = Mockito.mock(JarOutputStream.class); + + spyStrategy = Mockito.spy(new JarModuleExportStrategy()); + mockModuleInfo = MockEntities.makeMockPackageInfo(); + Mockito.doCallRealMethod().when(mockStrategy).export(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.when(mockStrategy.createTempFile(Mockito.any())).thenReturn(tempFolder.newFile(TEMP_FILE_NAME)); } @Test public void testExportWorksAndInvokesCreateTempFile() { - mockStrategy.export(moduleInfo, instrumented, null, null); - Mockito.verify(mockStrategy).createTempFile(moduleInfo); + mockStrategy.export(mockModuleInfo, instrumented, null); + Mockito.verify(mockStrategy).createTempFile(mockModuleInfo); } @Test public void testExportWorksAndInvokesBuildOutputJar() { - mockStrategy.export(moduleInfo, instrumented, null, null); + mockStrategy.export(mockModuleInfo, instrumented, null); Mockito.verify(mockStrategy).buildOutputJar(Mockito.any(), Mockito.any(), Mockito.any()); } @Test public void testExportWorksAndInvokesMoveTempFileToDestination() { - mockStrategy.export(moduleInfo, instrumented, null, null); - Mockito.verify(mockStrategy).moveTempFileToDestination(Mockito.eq(moduleInfo), Mockito.any(), Mockito.any()); + mockStrategy.export(mockModuleInfo, instrumented, null); + Mockito.verify(mockStrategy).moveTempFileToDestination(Mockito.eq(mockModuleInfo), Mockito.any(), Mockito.any()); } @Test public void testSaveTransformedClassesWorksAndCreatesNewEntries() throws IOException { - strategy.saveTransformedClasses(mockJarOS, MockEntities.makeInstrumentedClassesMap()); + spyStrategy.saveTransformedClasses(mockJarOS, MockEntities.makeInstrumentedClassesMap()); ArgumentCaptor jarEntryArgument = ArgumentCaptor.forClass(JarEntry.class); Mockito.verify(mockJarOS, Mockito.times(3)).putNextEntry(jarEntryArgument.capture()); @@ -109,7 +105,7 @@ public void testSaveTransformedClassesWorksAndCreatesNewEntries() throws IOExcep public void testCopyExistingJarEntriesWorksWithFilesAndPath() throws IOException { Mockito.doCallRealMethod().when(mockStrategy).copyExistingJarEntries(Mockito.eq(mockJarOS), Mockito.any(), Mockito.any()); - mockStrategy.copyExistingJarEntries(mockJarOS, moduleInfo, MockEntities.makeInstrumentedClassesMap()); + mockStrategy.copyExistingJarEntries(mockJarOS, mockModuleInfo, MockEntities.makeInstrumentedClassesMap()); // 3 out of 6 classes have not been instrumented Mockito.verify(mockStrategy, Mockito.times(3)).copyJarEntry(Mockito.eq(mockJarOS), Mockito.any(), Mockito.any()); @@ -124,7 +120,7 @@ public void testCopyExistingJarEntriesWorksWithFilesAndPath() throws IOException public void testCopyJarEntryFailsWithNullEntry() throws IOException { Mockito.doReturn(null).when(mockJarFile).getInputStream(Mockito.any()); - strategy.copyJarEntry(mockJarOS, mockJarFile, new JarEntry("fff")); + spyStrategy.copyJarEntry(mockJarOS, mockJarFile, new JarEntry("fff")); } @Test @@ -134,128 +130,84 @@ public void testCopyJarEntryWorksAndWritesToOS() throws IOException { Mockito.when(mockJarFile.getInputStream(null)).thenReturn(mockStream); Mockito.when(mockStream.read(Mockito.any())).thenReturn(1).thenReturn(-1); - strategy.copyJarEntry(mockJarOS, mockJarFile, null); + spyStrategy.copyJarEntry(mockJarOS, mockJarFile, null); Mockito.verify(mockJarOS).putNextEntry(null); Mockito.verify(mockJarOS).write(Mockito.any(), Mockito.eq(0), Mockito.eq(1)); Mockito.verify(mockJarOS).closeEntry(); } - @Test - public void testCreateTempFileWorks() { - File file = strategy.createTempFile(moduleInfo); - - Assert.assertNotNull(file); - file.delete(); - } - @Test public void testMoveTempFileToDestinationToReplaceOriginal() throws IOException { // create original file and assume temp/disco is where the original package is - File tempFile = createOriginalFile(); + File originalFile = createOriginalFile(); + Assert.assertEquals(1, originalFile.length()); - long originalLength = tempFile.length(); + long originalLength = originalFile.length(); - // create temp file named path.temp.jar - Mockito.when(moduleInfo.getFile()).thenReturn(mockFile); - Mockito.when(mockFile.getAbsolutePath()).thenReturn(ORIGINAL_FILE_PATH); - File file = strategy.createTempFile(moduleInfo); + // create temp file named temp.jar + Mockito.when(mockModuleInfo.getFile()).thenReturn(originalFile); + File file = spyStrategy.createTempFile(mockModuleInfo); // replace original file - Path path = strategy.moveTempFileToDestination(moduleInfo, null, file); + Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, null, file); Assert.assertNotEquals(originalLength, path.toFile().length()); - Assert.assertEquals(tempFile.getAbsolutePath(), path.toFile().getAbsolutePath()); - - file.delete(); - path.toFile().delete(); + Assert.assertEquals(originalFile.getAbsolutePath(), path.toFile().getAbsolutePath()); + Assert.assertEquals(0, path.toFile().length()); + Assert.assertEquals(0, originalFile.length()); } @Test public void testMoveTempFileToDestinationWorks() throws IOException { - String destination = DESTINATION_PATH; - strategy = new JarModuleExportStrategy(destination); + File outDir = tempFolder.newFolder(OUT_DIR); + spyStrategy = new JarModuleExportStrategy(outDir.getAbsolutePath()); // create original file and assume temp/disco/tests is where the original package is - File original = createOriginalFile(); + File originalFile = createOriginalFile(); + Assert.assertEquals(1, originalFile.length()); - File file = strategy.createTempFile(moduleInfo); + // create temp file named temp.jar + Mockito.when(mockModuleInfo.getFile()).thenReturn(originalFile); + File file = spyStrategy.createTempFile(mockModuleInfo); // move to destination - Path path = strategy.moveTempFileToDestination(moduleInfo, null, file); - - Assert.assertEquals(destination, path.toFile().getParentFile().getAbsolutePath()); - Assert.assertEquals(moduleInfo.getFile().getName(), path.toFile().getName()); - Assert.assertTrue(original.exists()); + Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, null, file); - file.delete(); - path.toFile().delete(); - original.delete(); + Assert.assertEquals(outDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); + Assert.assertEquals(mockModuleInfo.getFile().getName(), path.toFile().getName()); + Assert.assertEquals(ORIGINAL_FILE_NAME, path.toFile().getName()); + Assert.assertNotEquals(originalFile.length(), path.toFile().length()); + Assert.assertTrue(originalFile.exists()); } @Test public void testMoveTempFileToDestinationWorksWithSuffix() throws IOException { - String destination = DESTINATION_PATH; - strategy = new JarModuleExportStrategy(destination); - - // create original file and assume temp/disco/tests is where the original package is - File original = createOriginalFile(); - - File file = strategy.createTempFile(moduleInfo); - - // move to destination - Path path = strategy.moveTempFileToDestination(moduleInfo, PACKAGE_SUFFIX, file); - - String nameToCheck = moduleInfo.getFile() - .getName() - .substring(0, moduleInfo.getFile().getName().lastIndexOf(PreprocessConstants.JAR_EXTENSION)) - + PACKAGE_SUFFIX - + PreprocessConstants.JAR_EXTENSION; - - Assert.assertEquals(destination, path.toFile().getParentFile().getAbsolutePath()); - Assert.assertEquals(nameToCheck, path.toFile().getName()); - Assert.assertTrue(original.exists()); - - file.delete(); - path.toFile().delete(); - original.delete(); - } - - @Test(expected = ModuleExportException.class) - public void testMoveTempFileToDestinationFailsWhenFileAlreadyExistsWithSameName() throws IOException { - String destination = DESTINATION_PATH; - strategy = new JarModuleExportStrategy(destination); + File outputDir = tempFolder.newFolder(OUT_DIR); + spyStrategy = new JarModuleExportStrategy(outputDir.getAbsolutePath()); // create original file and assume temp/disco/tests is where the original package is - File original = createOriginalFile(); + File originalFile = createOriginalFile(); - File file = strategy.createTempFile(moduleInfo); + File tempFile = spyStrategy.createTempFile(mockModuleInfo); // move to destination - Path path = strategy.moveTempFileToDestination(moduleInfo, PACKAGE_SUFFIX, file); + Mockito.when(mockModuleInfo.getFile()).thenReturn(originalFile); + Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, PACKAGE_SUFFIX, tempFile); - String nameToCheck = moduleInfo.getFile() + String nameToCheck = mockModuleInfo.getFile() .getName() - .substring(0, moduleInfo.getFile().getName().lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + .substring(0, mockModuleInfo.getFile().getName().lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + PACKAGE_SUFFIX + PreprocessConstants.JAR_EXTENSION; - Assert.assertEquals(destination, path.toFile().getParentFile().getAbsolutePath()); + Assert.assertEquals(outputDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); Assert.assertEquals(nameToCheck, path.toFile().getName()); - Assert.assertTrue(original.exists()); - - // mock another package of the same name being moved to the same dir as the first one - try { - strategy.moveTempFileToDestination(moduleInfo, PACKAGE_SUFFIX, file); - } finally { - file.delete(); - path.toFile().delete(); - original.delete(); - } + Assert.assertTrue(originalFile.exists()); } private File createOriginalFile() throws IOException { - File tempFile = new File(ORIGINAL_FILE_PATH); + File tempFile = tempFolder.newFile(ORIGINAL_FILE_NAME); tempFile.getParentFile().mkdirs(); FileOutputStream os = new FileOutputStream(tempFile); diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java index 22ce423..05970c3 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java @@ -147,7 +147,7 @@ public void testApplyInstrumentationWorksAndInvokesExport() { spyTransformer.applyInstrumentation(moduleInfos.get(0)); Mockito.verify(moduleInfos.get(0)).getClassNames(); - Mockito.verify(s1).export(moduleInfos.get(0), instrumentedClasses, null, PACKAGE_SUFFIX); + Mockito.verify(s1).export(moduleInfos.get(0), instrumentedClasses, PACKAGE_SUFFIX); Assert.assertTrue(instrumentedClasses.isEmpty()); } } From d1834138883d11f4779a5c7e6a70b16acb01ec3b Mon Sep 17 00:00:00 2001 From: Connell Date: Fri, 17 Jul 2020 17:59:58 -0700 Subject: [PATCH 26/45] Add a runtimeOnly flag to the agent config, in support of scenarios where instrumentation is not required, such as when classes have been pre-transformed by a build tool. In this mode, the Agent is loaded only to provide a TransactionContext and EventBus runtime, added to the bootstrap classloader Retired the overlapping noDefaultInstallables flag, which was coupled to the idea of monolithically built agents. The runtimeOnly flag is a superset of it which also handles plugins. --- .../disco/agent/DiscoAgentTemplate.java | 8 ++--- .../disco/agent/config/AgentConfig.java | 22 ++++++++------ .../disco/agent/config/AgentConfigParser.java | 4 +-- .../disco/agent/plugin/PluginDiscovery.java | 19 ++++++++---- .../disco/agent/DiscoAgentTemplateTests.java | 29 ++++++++----------- .../agent/config/AgentConfigParserTest.java | 8 ++--- .../agent/plugin/PluginDiscoveryTests.java | 26 +++++++++++++++++ 7 files changed, 74 insertions(+), 42 deletions(-) diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/DiscoAgentTemplate.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/DiscoAgentTemplate.java index bb335f1..8290c3a 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/DiscoAgentTemplate.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/DiscoAgentTemplate.java @@ -39,9 +39,9 @@ * agent able to configure which actual Installable hooks are present in the Agent instance. */ public class DiscoAgentTemplate { - private static Logger log = LogManager.getLogger(DiscoAgentTemplate.class); + private static final Logger log = LogManager.getLogger(DiscoAgentTemplate.class); - private AgentConfig config; + private final AgentConfig config; private InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); private ElementMatcher.Junction customIgnoreMatcher = ElementMatchers.none(); private boolean allowPlugins = true; @@ -110,8 +110,8 @@ public Collection install(Instrumentation instrumentation, Set install(Instrumentation instrumentation, Set installables, ElementMatcher.Junction customIgnoreMatcher) { - if (!config.isInstallDefaultInstallables()) { - log.info("DiSCo(Core) removing all default installables as requested"); + if (config.isRuntimeOnly()) { + log.info("DiSCo(Core) setting agent as runtime-only. Ignoring all Installables, including those in Plugins."); installables.clear(); } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java index cab08b8..f9f6131 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java @@ -22,7 +22,7 @@ */ public class AgentConfig { private List args; - private boolean installDefaultInstallables = true; + private boolean isRuntimeOnly = false; private String pluginPath = null; private boolean verbose = false; private boolean extraverbose = false; @@ -46,11 +46,15 @@ public List getArgs() { } /** - * Return if we are configured to install the default installables (i.e. the ones expressly listed out in the Agent startup). - * @return true if default installables should be installed, else false + * Return true if configured to perform no instrumentation of classes. The agent in this mode only acts as a way to install + * the Disco runtime (TransactionContext, EventBus and so on) such that it is on the correct classloader as demanded by + * the agent manifest (usually the bootstrap such that Concurrency support works correctly). This mode is used when running an + * application which does not need runtime instrumentation, e.g. if all Event-publishing classes are integrated explicitly, or + * if classes have been transformed ahead of time by a build tool. + * @return true if the agent should be a container for the runtime only, thus disabling all Installable interceptions */ - public boolean isInstallDefaultInstallables() { - return installDefaultInstallables; + public boolean isRuntimeOnly() { + return isRuntimeOnly; } /** @@ -78,11 +82,11 @@ public boolean isExtraverbose() { } /** - * Set whether to install the default installables for this agent - * @param installDefaultInstallables true to install the default installables, else false + * Set whether this Agent should install no installables and be a runtime-only agent. + * @param isRuntimeOnly true for a runtime-only agent, else false (the default) */ - protected void setInstallDefaultInstallables(boolean installDefaultInstallables) { - this.installDefaultInstallables = installDefaultInstallables; + protected void setRuntimeOnly(boolean isRuntimeOnly) { + this.isRuntimeOnly = isRuntimeOnly; } /** diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfigParser.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfigParser.java index 0347f8b..577ebe3 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfigParser.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfigParser.java @@ -51,8 +51,8 @@ public AgentConfig parseCommandLine(String args) { : ""; switch (pair[0].toLowerCase()) { - case "nodefaultinstallables": - result.setInstallDefaultInstallables(false); + case "runtimeonly": + result.setRuntimeOnly(true); break; case "pluginpath": result.setPluginPath(value); diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java index ebdf71f..3807e8c 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/plugin/PluginDiscovery.java @@ -114,7 +114,7 @@ public static void scan(Instrumentation instrumentation, AgentConfig config) { if (files != null) { for (File jarFile : files) { if (jarFile.getName().substring(jarFile.getName().lastIndexOf(".")).equalsIgnoreCase(".jar")) { - processJarFile(instrumentation, jarFile); + processJarFile(instrumentation, jarFile, config.isRuntimeOnly()); } else { //ignore non JAR file log.info("DiSCo(Core) non JAR file found on plugin path, skipping this file"); @@ -196,9 +196,10 @@ public static Collection apply() { * Process a single JAR file which is assumed to be a plugin * @param instrumentation and instrumentation instance, used to add discovered plugins to classpaths * @param jarFile the jar file to be processed + * @param runtimeOnly if the Agent is configured as runtime only, Installables will not be considered from plugins. Init plugins and Listener plugins are unaffected. * @throws Exception class reflection or file i/o errors may occur */ - static void processJarFile(Instrumentation instrumentation, File jarFile) throws Exception { + static void processJarFile(Instrumentation instrumentation, File jarFile, boolean runtimeOnly) throws Exception { JarFile jar = new JarFile(jarFile); Manifest manifest = jar.getManifest(); jar.close(); @@ -237,7 +238,7 @@ static void processJarFile(Instrumentation instrumentation, File jarFile) throws boolean bootstrap = loadJar(instrumentation, jarFile, bootstrapClassloader); pluginOutcomes.get(pluginName).bootstrap = bootstrap; processInitClass(pluginName, initClassName, bootstrap); - processInstallableClasses(pluginName, installableClassNames, bootstrap); + processInstallableClasses(pluginName, installableClassNames, bootstrap, runtimeOnly); processListenerClasses(pluginName, listenerClassNames, bootstrap); } @@ -296,12 +297,18 @@ static void processInitClass(String pluginName, String initClassName, boolean bo * @param pluginName the name of the plugin JAR file where the classes are defined * @param installableClassNames the names of the Installable or Package classes determined from the Manifest * @param bootstrap true if the plugin is requesting to be loaded by the bootstrap classloader + * @param runtimeOnly true if the agent is configured to be runtime only, thus no Installables will be used for instrumentation. * @throws Exception reflection errors may occur if the class cannot be found */ - static void processInstallableClasses(String pluginName, String installableClassNames, boolean bootstrap) throws Exception { + static void processInstallableClasses(String pluginName, String installableClassNames, boolean bootstrap, boolean runtimeOnly) throws Exception { if (installableClassNames != null) { String[] classNames = splitString(installableClassNames); for (String className: classNames) { + if (runtimeOnly) { + log.info("DiSCo(Core) Installable/Package declared in plugin will be ignored because agent is configured as runtime only: " + className); + continue; + } + try { Class clazz = classForName(className.trim(), bootstrap); if (Installable.class.isAssignableFrom(clazz) || Package.class.isAssignableFrom(clazz)) { @@ -311,7 +318,7 @@ static void processInstallableClasses(String pluginName, String installableClass log.warn("DiSCo(Core) specified Installable is not an instance of Installable or Package: " + className); } } catch (ClassNotFoundException e) { - log.warn("DiSCo(Core) cannot locate Installable: " + className); + log.warn("DiSCo(Core) cannot locate Installable: " + className, e); } } } @@ -340,7 +347,7 @@ static void processListenerClasses(String pluginName, String listenerClassNames, log.warn("DiSCo(Core) specified Listener is not an instance of Listener: " + className); } } catch (ClassNotFoundException e) { - log.warn("DiSCo(Core) failed to instantiate Listener: " + className); + log.warn("DiSCo(Core) failed to instantiate Listener: " + className, e); } } } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java index b89adad..c829e09 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java @@ -43,12 +43,17 @@ public class DiscoAgentTemplateTests { @Mock private InterceptionInstaller mockInterceptionInstaller; + + @Mock + private Instrumentation instrumentation; + @Captor private ArgumentCaptor> installableSetArgumentCaptor; @Before public void before() { MockitoAnnotations.initMocks(this); + Mockito.doCallRealMethod().when(mockInterceptionInstaller).install(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); TransactionContext.create(); } @@ -57,23 +62,15 @@ public void after() { TransactionContext.clear(); } - @Test - public void testDefaultInstallables() { - Installable mockInstallable = Mockito.mock(Installable.class); - Set installables = new HashSet<>(); - installables.add(mockInstallable); - install(createDiscoAgentTemplate(), installables); - Mockito.verify(mockInterceptionInstaller).install(Mockito.any(), installableSetArgumentCaptor.capture(), Mockito.any(), Mockito.any()); - Assert.assertTrue(installableSetArgumentCaptor.getValue().contains(mockInstallable)); - } @Test - public void testNoDefaultInstallables() { - Installable mockInstallable = Mockito.mock(Installable.class); + public void testRuntimeOnly() { + Installable mockInstallable = new DummyInstallable(); Set installables = new HashSet<>(); installables.add(mockInstallable); - install(createDiscoAgentTemplate("noDefaultInstallables"), installables); + install(createDiscoAgentTemplate("runtimeOnly"), installables); Mockito.verify(mockInterceptionInstaller).install(Mockito.any(), installableSetArgumentCaptor.capture(), Mockito.any(), Mockito.any()); + Mockito.verifyNoInteractions(instrumentation); Assert.assertTrue(installableSetArgumentCaptor.getValue().isEmpty()); } @@ -107,7 +104,7 @@ private DiscoAgentTemplate createDiscoAgentTemplate(String... args) { } private DiscoAgentTemplate install(DiscoAgentTemplate discoAgentTemplate, Set installables) { - discoAgentTemplate.install(Mockito.mock(Instrumentation.class), installables); + discoAgentTemplate.install(instrumentation, installables); return discoAgentTemplate; } @@ -115,12 +112,10 @@ private DiscoAgentTemplate install(DiscoAgentTemplate discoAgentTemplate) { return install(discoAgentTemplate, new HashSet<>()); } - //not genuinely using this object, just using it to produce a classname which implements Installable - //Cannot use a Mockito 1.x mock due to the reliance on the default method in the ArgumentHandler base interface. - static class MockInstallable implements Installable { + static class DummyInstallable implements Installable { @Override public AgentBuilder install(AgentBuilder agentBuilder) { - return null; + return agentBuilder; } } } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigParserTest.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigParserTest.java index 9214287..48fe24d 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigParserTest.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigParserTest.java @@ -17,17 +17,17 @@ import org.junit.Test; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class AgentConfigParserTest { @Test public void testArgumentParsing() { - final String argLine = "agent.jar:verbose:nodefaultinstallables"; + final String argLine = "verbose:runtimeonly:pluginpath=path/to/plugins"; AgentConfig config = new AgentConfigParser().parseCommandLine(argLine); - assertFalse(config.isInstallDefaultInstallables()); + assertTrue(config.isRuntimeOnly()); assertTrue(config.isVerbose()); assertFalse(config.isExtraverbose()); + assertEquals("path/to/plugins", config.getPluginPath()); } } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java index 36ff4da..1798afd 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/plugin/PluginDiscoveryTests.java @@ -127,6 +127,32 @@ public void testPluginPackageInstallableNonBootstrap() throws Exception { Assert.assertTrue(classes.contains(PluginPackage.OtherInstallable.class)); } + @Test + public void testPluginInstallableRuntimeOnly() throws Exception { + agentConfig = new AgentConfigParser().parseCommandLine("runtimeOnly"); + createJar("plugin_runtime_only", + "Disco-Installable-Classes: software.amazon.disco.agent.plugin.source.PluginInstallable", + "software.amazon.disco.agent.plugin.source.PluginInstallable"); + Collection outcomes = scanAndApply(instrumentation, agentConfig); + Assert.assertTrue(outcomes.isEmpty()); + Assert.assertTrue(installables.isEmpty()); + Mockito.verifyNoInteractions(instrumentation); + } + + @Test + public void testPluginPackageInstallableRuntimeOnly() throws Exception { + agentConfig = new AgentConfigParser().parseCommandLine("runtimeOnly"); + createJar("plugin_package_runtime_only", + "Disco-Installable-Classes: software.amazon.disco.agent.plugin.source.PluginPackage", + "software.amazon.disco.agent.plugin.source.PluginPackage", + "software.amazon.disco.agent.plugin.source.PluginInstallable", + "software.amazon.disco.agent.plugin.source.PluginPackage$OtherInstallable"); + Collection outcomes = scanAndApply(instrumentation, agentConfig); + Assert.assertTrue(outcomes.isEmpty()); + Assert.assertTrue(installables.isEmpty()); + Mockito.verifyNoInteractions(instrumentation); + } + @Test public void testPluginBootstrapFlag() throws Exception { createJar("plugin_with_bootstrap_true", From 01df46dbe9b0a93e1cc6d447199ca73bebf5fc2a Mon Sep 17 00:00:00 2001 From: Connell Date: Tue, 21 Jul 2020 18:24:08 -0700 Subject: [PATCH 27/45] Add a Factory indirection to InterceptionInstaller, to aid with testing. Add new test coverage along with it --- .../interception/InterceptionInstaller.java | 31 ++++++- .../disco/agent/DiscoAgentTemplateTests.java | 3 +- .../InterceptionInstallerTests.java | 81 ++++++++++++++++++- 3 files changed, 111 insertions(+), 4 deletions(-) diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java index d10c4b8..40d395a 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java @@ -24,6 +24,7 @@ import java.lang.instrument.Instrumentation; import java.util.Set; +import java.util.function.Supplier; import static net.bytebuddy.matcher.ElementMatchers.*; @@ -33,12 +34,13 @@ public class InterceptionInstaller { private static final InterceptionInstaller INSTANCE = new InterceptionInstaller(); private static final Logger log = LogManager.getLogger(InterceptionInstaller.class); + private Supplier agentBuilderFactory; /** * Private constructor for singleton semantics */ private InterceptionInstaller() { - + agentBuilderFactory = new DefaultAgentBuilderFactory(); } /** @@ -64,7 +66,7 @@ public void install(Instrumentation instrumentation, Set installabl for (Installable installable: installables) { //We create a new Agent for each Installable, otherwise their matching rules can //compete with each other. - AgentBuilder agentBuilder = new AgentBuilder.Default() + AgentBuilder agentBuilder = agentBuilderFactory.get() .ignore(ignoreMatcher); //The Interception listener is expensive during class loading, and limited value most of the time @@ -105,4 +107,29 @@ public static ElementMatcher.Junction createIgnoreMatch return excludedNamespaces.or(customIgnoreMatcher); } + + /** + * Override the default AgentBuilder factory. Expected to be used only in tests, so package-private. + * @param agentBuilderFactory the new (probably mock) AgentBuilder factory to use. + * @return the previous AgentBuilder factory + */ + Supplier setAgentBuilderFactory(Supplier agentBuilderFactory) { + Supplier ret = this.agentBuilderFactory; + this.agentBuilderFactory = agentBuilderFactory; + return ret; + } + + /** + * A default Factory for creation of AgentBuilder instances + */ + private static class DefaultAgentBuilderFactory implements Supplier { + /** + * Factory method to produce a real AgentBuilder + * @return an AgentBuilder in the default case + */ + @Override + public AgentBuilder get() { + return new AgentBuilder.Default(); + } + } } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java index c829e09..f22a6d8 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java @@ -15,6 +15,7 @@ package software.amazon.disco.agent; +import org.mockito.Spy; import software.amazon.disco.agent.concurrent.TransactionContext; import software.amazon.disco.agent.interception.Installable; import software.amazon.disco.agent.interception.InterceptionInstaller; @@ -41,7 +42,7 @@ import static org.junit.Assert.assertEquals; public class DiscoAgentTemplateTests { - @Mock + @Spy private InterceptionInstaller mockInterceptionInstaller; @Mock diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java index 4dd932f..9d6c7bb 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java @@ -15,20 +15,27 @@ package software.amazon.disco.agent.interception; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.matcher.ElementMatcher; +import org.mockito.Mockito; import software.amazon.disco.agent.config.AgentConfig; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatchers; import org.junit.Assert; import org.junit.Test; +import software.amazon.disco.agent.config.AgentConfigParser; +import java.lang.instrument.Instrumentation; +import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.function.Supplier; public class InterceptionInstallerTests { @Test public void testIgnoreMatcherMatchesJavaInternals() throws Exception { //random selections from each of the sun, com.sun and jdk namespaces - //ForName is used to prevent warnings such as "warning: AbstractMultiResolutionImage is internal proprietary API and may be removed in a future release" + //ForName is used to prevent warnings such as "warning: such-and-such-class is internal proprietary API and may be removed in a future release", or deprecation warnings. Assert.assertTrue(classMatches(Class.forName("sun.misc.Unsafe"))); Assert.assertTrue(classMatches(Class.forName("com.sun.awt.SecurityWarning"))); Assert.assertTrue(classMatches(Class.forName("jdk.nashorn.api.scripting.AbstractJSObject"))); @@ -61,7 +68,79 @@ public void testNullAgentBuilderIsSafe() { interceptionInstaller.install(null, installables, new AgentConfig(null), ElementMatchers.none()); } + @Test + public void testAgentBuilderHasDefaultIgnoreMatcher() { + ElementMatcher ignoreMatcher = InterceptionInstaller.createIgnoreMatcher(ElementMatchers.none()); + InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); + AgentBuilder agentBuilder = Mockito.mock(AgentBuilder.class); + Supplier original = interceptionInstaller.setAgentBuilderFactory(()->agentBuilder); + interceptionInstaller.install(Mockito.mock(Instrumentation.class), new HashSet<>(Arrays.asList((a)->a)), new AgentConfig(null), ElementMatchers.none()); + Mockito.verify(agentBuilder).ignore(Mockito.eq(ignoreMatcher)); + interceptionInstaller.setAgentBuilderFactory(original); + } + + @Test + public void testAgentBuilderNotHasListenerWhenNotVerbose() { + InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); + MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); + Supplier original = interceptionInstaller.setAgentBuilderFactory(factory); + interceptionInstaller.install(Mockito.mock(Instrumentation.class), new HashSet<>(Arrays.asList((a)->a)), new AgentConfig(null), ElementMatchers.none()); + Mockito.verify(factory.agentBuilder, Mockito.never()).with(Mockito.any(AgentBuilder.Listener.class)); + interceptionInstaller.setAgentBuilderFactory(original); + } + + @Test + public void testAgentBuilderHasListenerWhenVerbose() { + InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); + MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); + Supplier original = interceptionInstaller.setAgentBuilderFactory(factory); + AgentConfig agentConfig = new AgentConfigParser().parseCommandLine("extraverbose"); + interceptionInstaller.install(Mockito.mock(Instrumentation.class), new HashSet<>(Arrays.asList((a)->a)), agentConfig, ElementMatchers.none()); + Mockito.verify(factory.agentBuilder).with(Mockito.any(AgentBuilder.Listener.class)); + interceptionInstaller.setAgentBuilderFactory(original); + } + + @Test + public void testAgentBuilderIsInstalledOnInstrumentation() { + InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); + MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); + Supplier original = interceptionInstaller.setAgentBuilderFactory(factory); + Instrumentation instrumentation = Mockito.mock(Instrumentation.class); + interceptionInstaller.install(instrumentation, new HashSet<>(Arrays.asList((a)->a)), new AgentConfig(null), ElementMatchers.none()); + Mockito.verify(factory.agentBuilder).installOn(instrumentation); + interceptionInstaller.setAgentBuilderFactory(original); + } + + + @Test + public void testInstallablesInstallMethodCalled() { + InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); + MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); + Supplier original = interceptionInstaller.setAgentBuilderFactory(factory); + Instrumentation instrumentation = Mockito.mock(Instrumentation.class); + Set installables = new HashSet<>(Arrays.asList(Mockito.mock(Installable.class))); + interceptionInstaller.install(instrumentation, installables, new AgentConfig(null), ElementMatchers.none()); + for (Installable installable: installables) { + Mockito.verify(installable).install(factory.agentBuilder); + } + interceptionInstaller.setAgentBuilderFactory(original); + } + private boolean classMatches(Class clazz) { return InterceptionInstaller.createIgnoreMatcher(ElementMatchers.none()).matches(new TypeDescription.ForLoadedType(clazz)); } + + private static class MockAgentBuilderFactory implements Supplier { + public final AgentBuilder agentBuilder; + + public MockAgentBuilderFactory() { + AgentBuilder.Ignored agentBuilder = Mockito.mock(AgentBuilder.Ignored.class); + Mockito.when(agentBuilder.ignore(Mockito.any(ElementMatcher.class))).thenReturn(agentBuilder); + this.agentBuilder = agentBuilder; + } + @Override + public AgentBuilder get() { + return agentBuilder; + } + } } From 514778faf0fb82fbbab4ea754bdf17c395bb4683 Mon Sep 17 00:00:00 2001 From: Hongbo Liu Date: Tue, 21 Jul 2020 19:12:36 -0400 Subject: [PATCH 28/45] implemented extension point in core via AgentConfig to transform AgentBuilder pre installation --- .../loaders/agents/DiscoAgentLoader.java | 26 +++- .../loaders/agents/DiscoAgentLoaderTest.java | 18 +++ .../disco/agent/config/AgentConfig.java | 36 +++++ .../interception/InterceptionInstaller.java | 6 +- .../disco/agent/config/AgentConfigTest.java | 35 +++++ .../InterceptionInstallerTests.java | 138 +++++++++++++++++- 6 files changed, 251 insertions(+), 8 deletions(-) create mode 100644 disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigTest.java diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java index 9d9ff64..06b4799 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java @@ -15,8 +15,14 @@ package software.amazon.disco.instrumentation.preprocess.loaders.agents; +import software.amazon.disco.agent.config.AgentConfig; import software.amazon.disco.agent.inject.Injector; +import software.amazon.disco.agent.interception.Installable; import software.amazon.disco.instrumentation.preprocess.exceptions.NoPathProvidedException; +import software.amazon.disco.instrumentation.preprocess.instrumentation.TransformationListener; + +import java.io.File; +import java.lang.instrument.Instrumentation; /** * Agent loader used to dynamically load a Java Agent at runtime by calling the @@ -39,11 +45,27 @@ public DiscoAgentLoader(final String path) { /** * {@inheritDoc} - * Install a monolithic agent by directly invoking the {@link Injector} api. + * Install an agent by directly invoking the {@link Injector} api. */ @Override public void loadAgent() { - Injector.loadAgent(path, null); + final Instrumentation instrumentation = Injector.createInstrumentation(); + Injector.addToBootstrapClasspath(instrumentation, new File(path)); + + AgentConfig.setAgentBuilderTransformer( + (agentBuilder, installable) -> agentBuilder.with(new TransformationListener(uuidGenerate(installable)))); + + Injector.loadAgent(instrumentation, path, null); + } + + /** + * Generate a uuid to identify the {@link Installable} being passed in. + * + * @param installable an Installable that will have a TransformationListener installed on. + * @return a uuid that identifies the Installable passed in. + */ + private String uuidGenerate(Installable installable) { + return "mock uuid"; //TODO } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java index ce6023c..c726c7f 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java @@ -15,12 +15,30 @@ package software.amazon.disco.instrumentation.preprocess.loaders.agents; +import net.bytebuddy.agent.builder.AgentBuilder; +import org.junit.Assert; import org.junit.Test; +import org.mockito.Mockito; +import software.amazon.disco.agent.config.AgentConfig; +import software.amazon.disco.agent.interception.Installable; import software.amazon.disco.instrumentation.preprocess.exceptions.NoPathProvidedException; +import software.amazon.disco.instrumentation.preprocess.instrumentation.TransformationListener; public class DiscoAgentLoaderTest { @Test(expected = NoPathProvidedException.class) public void testConstructorFailOnNullPaths() throws NoPathProvidedException { new DiscoAgentLoader(null); } + + @Test + public void testLoadAgentCallsSetAgentBuilderTransformer(){ + DiscoAgentLoader loader = Mockito.spy(new DiscoAgentLoader("a path")); + AgentBuilder builder = Mockito.mock(AgentBuilder.class); + Installable installable = Mockito.mock(Installable.class); + + loader.loadAgent(); + new AgentConfig(null).getAgentBuilderTransformer().apply(builder, installable); + + Mockito.verify(builder).with(Mockito.any(TransformationListener.class)); + } } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java index f9f6131..3470460 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java @@ -15,12 +15,18 @@ package software.amazon.disco.agent.config; +import net.bytebuddy.agent.builder.AgentBuilder; +import software.amazon.disco.agent.interception.Installable; + import java.util.List; +import java.util.function.BiFunction; /** * Holds agent configuration parsed during bootstrap. */ public class AgentConfig { + private static BiFunction agentBuilderTransformer = new NoOpAgentBuilderTransformer(); + private List args; private boolean isRuntimeOnly = false; private String pluginPath = null; @@ -37,6 +43,36 @@ public AgentConfig(List args) { this.args = args; } + /** + * A default, 'identity', AgentBuilderTransformer, that just returns the inputted AgentBuilder + */ + private static class NoOpAgentBuilderTransformer implements BiFunction { + @Override + public AgentBuilder apply(AgentBuilder agentBuilder, Installable installable) { + return agentBuilder; + } + } + + /** + * Set a Transformer (a function taking an AgentBuilder, an Installable, and returning an AgentBuilder). + * This transformer will be invoked in InterceptionInstaller to apply transformations on all Installables of + * a given agent. Passing a null value will reset the AgentBuilderTransformer to the default value: {@link NoOpAgentBuilderTransformer} + * + * @param agentBuilderTransformer the AgentBuilder Transformer to be applied to an AgentBuilder + */ + public static void setAgentBuilderTransformer(BiFunction agentBuilderTransformer) { + AgentConfig.agentBuilderTransformer = agentBuilderTransformer == null ? new NoOpAgentBuilderTransformer() : agentBuilderTransformer; + } + + /** + * Get the registered Transformer (a function taking an AgentBuilder, an Installable, and returning an AgentBuilder) + * + * @return a transformed AgentBuilder instance, which may not be the same instance that was passed. + */ + public BiFunction getAgentBuilderTransformer() { + return AgentConfig.agentBuilderTransformer; + } + /** * Get the list of arguments which were given to the command line e.g. ["key1=value1", "key2=value2,value3", "value4"] * @return command line arguments diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java index 40d395a..842caee 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java @@ -60,8 +60,7 @@ public static InterceptionInstaller getInstance() { */ public void install(Instrumentation instrumentation, Set installables, AgentConfig config, ElementMatcher.Junction customIgnoreMatcher) { - ElementMatcher ignoreMatcher = createIgnoreMatcher(customIgnoreMatcher); - + final ElementMatcher ignoreMatcher = createIgnoreMatcher(customIgnoreMatcher); for (Installable installable: installables) { //We create a new Agent for each Installable, otherwise their matching rules can @@ -74,8 +73,11 @@ public void install(Instrumentation instrumentation, Set installabl agentBuilder = agentBuilder.with(InterceptionListener.create(installable)); } + agentBuilder = config.getAgentBuilderTransformer().apply(agentBuilder, installable); + log.info("DiSCo(Core) attempting to install "+installable.getClass().getName()); agentBuilder = installable.install(agentBuilder); + if (agentBuilder != null) { agentBuilder.installOn(instrumentation); } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigTest.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigTest.java new file mode 100644 index 0000000..a11cd7a --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigTest.java @@ -0,0 +1,35 @@ +package software.amazon.disco.agent.config; + +import net.bytebuddy.agent.builder.AgentBuilder; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import software.amazon.disco.agent.interception.Installable; + +import java.util.function.BiFunction; + +public class AgentConfigTest { + static AgentConfig config = new AgentConfig(null); + + @Test + public void testDefaultAgentBuilderTransformer() { + Assert.assertNotNull(config.getAgentBuilderTransformer()); + } + + @Test + public void testSetNonDefaultAgentBuilderTransformer() { + BiFunction defaultTransformer = config.getAgentBuilderTransformer(); + BiFunction mockTransformer = Mockito.mock(BiFunction.class); + + AgentConfig.setAgentBuilderTransformer(mockTransformer); + + Assert.assertNotEquals(defaultTransformer, config.getAgentBuilderTransformer()); + } + + @Test + public void testSetNullAgentBuilderTransformerResetsToDefaultTransformer(){ + AgentConfig.setAgentBuilderTransformer(null); + + Assert.assertNotNull(config.getAgentBuilderTransformer()); + } +} diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java index 9d6c7bb..741b211 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java @@ -16,19 +16,21 @@ package software.amazon.disco.agent.interception; import net.bytebuddy.agent.builder.AgentBuilder; -import net.bytebuddy.matcher.ElementMatcher; -import org.mockito.Mockito; -import software.amazon.disco.agent.config.AgentConfig; import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; import org.junit.Assert; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import software.amazon.disco.agent.config.AgentConfig; import software.amazon.disco.agent.config.AgentConfigParser; import java.lang.instrument.Instrumentation; import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Supplier; public class InterceptionInstallerTests { @@ -62,7 +64,7 @@ public void testIgnoreMatcherNotMatches() { @Test public void testNullAgentBuilderIsSafe() { InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); - Installable installable = (agentBuilder)->null; + Installable installable = (agentBuilder) -> null; Set installables = new HashSet<>(); installables.add(installable); interceptionInstaller.install(null, installables, new AgentConfig(null), ElementMatchers.none()); @@ -126,6 +128,134 @@ public void testInstallablesInstallMethodCalled() { interceptionInstaller.setAgentBuilderFactory(original); } + @Test + public void testInstallWorksWithACollectionOfInstallables() { + InterceptionInstaller interceptionInstaller = Mockito.spy(InterceptionInstaller.getInstance()); + Instrumentation instrumentation = Mockito.mock(Instrumentation.class); + + Installable installable_a = Mockito.mock(Installable.class); + Installable installable_b = Mockito.mock(Installable.class); + AgentBuilder builder_a = Mockito.mock(AgentBuilder.class); + AgentBuilder builder_b = Mockito.mock(AgentBuilder.class); + + Mockito.doReturn(builder_a).when(installable_a).install(Mockito.any()); + Mockito.doReturn(builder_b).when(installable_b).install(Mockito.any()); + + interceptionInstaller.install(instrumentation, new HashSet<>(Arrays.asList(installable_a, installable_b)), new AgentConfig(null), null); + + Mockito.verify(installable_a).install(Mockito.any()); + Mockito.verify(installable_b).install(Mockito.any()); + + Mockito.verify(builder_a).installOn(instrumentation); + Mockito.verify(builder_b).installOn(instrumentation); + } + + @Test + public void testDefaultAgentBuilderTransformerDoesNothing() { + InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); + + MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); + interceptionInstaller.setAgentBuilderFactory(factory); + + AgentBuilder builder = factory.get(); + AgentConfig agentConfig = Mockito.spy(new AgentConfig(null)); + Installable installable = Mockito.mock(Installable.class); + + //spy on the real default transformer + BiFunction agentBuilderTransformer = Mockito.spy(agentConfig.getAgentBuilderTransformer()); + + interceptionInstaller.setAgentBuilderFactory(factory); + + //return the spied instance of the default transformer instead. + Mockito.when(agentConfig.getAgentBuilderTransformer()).thenReturn(agentBuilderTransformer); + + interceptionInstaller.install( + Mockito.mock(Instrumentation.class), + new HashSet(Arrays.asList(installable)), + agentConfig, + ElementMatchers.none()); + + Mockito.verify(agentBuilderTransformer).apply(builder, installable); + + //first interaction occurs when an ignore matcher is being added to the builder which is an intended operation. + Mockito.verify(builder).ignore(Mockito.any(ElementMatcher.class)); + Mockito.verifyNoMoreInteractions(builder); + Mockito.verify(installable).install(Mockito.eq(builder)); + } + + @Test + public void testNonDefaultAgentBuilderTransformerReturningSameInstance(){ + InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); + + MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); + interceptionInstaller.setAgentBuilderFactory(factory); + + AgentBuilder builder = factory.get(); + AgentConfig agentConfig = Mockito.spy(new AgentConfig(null)); + Installable installable = Mockito.mock(Installable.class); + + BiFunction agentBuilderTransformer = Mockito.spy(new BiFunction() { + @Override + public AgentBuilder apply(AgentBuilder agentBuilder, Installable installable) { + agentBuilder.disableClassFormatChanges(); + return agentBuilder; + } + }); + + Mockito.when(agentConfig.getAgentBuilderTransformer()).thenReturn(agentBuilderTransformer); + + interceptionInstaller.install( + Mockito.mock(Instrumentation.class), + new HashSet(Arrays.asList(installable)), + agentConfig, + ElementMatchers.none()); + + Mockito.verify(agentBuilderTransformer).apply(builder, installable); + + //first interaction occurs when an ignore matcher is being added to the builder which is an intended operation. + Mockito.verify(builder).ignore(Mockito.any(ElementMatcher.class)); + Mockito.verify(builder).disableClassFormatChanges(); + Mockito.verify(installable).install(Mockito.eq(builder)); + } + + @Test + public void testNonDefaultAgentBuilderTransformerReturningDifferentInstance(){ + InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); + + MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); + interceptionInstaller.setAgentBuilderFactory(factory); + + AgentBuilder originalBuilder = factory.get(); + AgentBuilder differentBuilder = Mockito.mock(AgentBuilder.class); + AgentConfig agentConfig = Mockito.spy(new AgentConfig(null)); + Installable installable = Mockito.mock(Installable.class); + + BiFunction agentBuilderTransformer = Mockito.spy(new BiFunction() { + @Override + public AgentBuilder apply(AgentBuilder agentBuilder, Installable installable) { + return differentBuilder; + } + }); + + Mockito.when(agentConfig.getAgentBuilderTransformer()).thenReturn(agentBuilderTransformer); + + interceptionInstaller.install( + Mockito.mock(Instrumentation.class), + new HashSet(Arrays.asList(installable)), + agentConfig, + ElementMatchers.none()); + + Mockito.verify(agentBuilderTransformer).apply(originalBuilder, installable); + + //first interaction occurs when an ignore matcher is being added to the builder which is an intended operation. + Mockito.verify(originalBuilder).ignore(Mockito.any(ElementMatcher.class)); + + ArgumentCaptor agentBuilderArgumentCaptor = ArgumentCaptor.forClass(AgentBuilder.class); + Mockito.verify(installable).install(agentBuilderArgumentCaptor.capture()); + Assert.assertEquals(differentBuilder, agentBuilderArgumentCaptor.getValue()); + Assert.assertNotEquals(originalBuilder, differentBuilder); + } + private boolean classMatches(Class clazz) { return InterceptionInstaller.createIgnoreMatcher(ElementMatchers.none()).matches(new TypeDescription.ForLoadedType(clazz)); } From 4c66fa6e51d4dcaaf7eb00818ffc1f03ccc19dcb Mon Sep 17 00:00:00 2001 From: Connell Date: Fri, 24 Jul 2020 11:48:09 -0700 Subject: [PATCH 29/45] Instantiate InterceptionInstaller in tests, to reduce reliance on global state --- .../interception/InterceptionInstaller.java | 21 +++------- .../disco/agent/DiscoAgentTemplateTests.java | 2 +- .../InterceptionInstallerTests.java | 42 +++++++------------ 3 files changed, 20 insertions(+), 45 deletions(-) diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java index 842caee..3defae0 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/InterceptionInstaller.java @@ -32,15 +32,15 @@ * Class to control installation of interceptions/advice on target methods. */ public class InterceptionInstaller { - private static final InterceptionInstaller INSTANCE = new InterceptionInstaller(); + private static final InterceptionInstaller INSTANCE = new InterceptionInstaller(new DefaultAgentBuilderFactory()); private static final Logger log = LogManager.getLogger(InterceptionInstaller.class); - private Supplier agentBuilderFactory; + private final Supplier agentBuilderFactory; /** - * Private constructor for singleton semantics + * Non-public constructor for singleton semantics. Package-private for tests */ - private InterceptionInstaller() { - agentBuilderFactory = new DefaultAgentBuilderFactory(); + InterceptionInstaller(Supplier agentBuilderFactory) { + this.agentBuilderFactory = agentBuilderFactory; } /** @@ -110,17 +110,6 @@ public static ElementMatcher.Junction createIgnoreMatch } - /** - * Override the default AgentBuilder factory. Expected to be used only in tests, so package-private. - * @param agentBuilderFactory the new (probably mock) AgentBuilder factory to use. - * @return the previous AgentBuilder factory - */ - Supplier setAgentBuilderFactory(Supplier agentBuilderFactory) { - Supplier ret = this.agentBuilderFactory; - this.agentBuilderFactory = agentBuilderFactory; - return ret; - } - /** * A default Factory for creation of AgentBuilder instances */ diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java index f22a6d8..decb144 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java @@ -43,7 +43,7 @@ public class DiscoAgentTemplateTests { @Spy - private InterceptionInstaller mockInterceptionInstaller; + private InterceptionInstaller mockInterceptionInstaller = InterceptionInstaller.getInstance(); @Mock private Instrumentation instrumentation; diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java index 741b211..bd4305d 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/InterceptionInstallerTests.java @@ -63,8 +63,9 @@ public void testIgnoreMatcherNotMatches() { @Test public void testNullAgentBuilderIsSafe() { - InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); - Installable installable = (agentBuilder) -> null; + AgentBuilder agentBuilder = Mockito.mock(AgentBuilder.class); + InterceptionInstaller interceptionInstaller = new InterceptionInstaller(()->agentBuilder); + Installable installable = (ab) -> null; Set installables = new HashSet<>(); installables.add(installable); interceptionInstaller.install(null, installables, new AgentConfig(null), ElementMatchers.none()); @@ -73,64 +74,55 @@ public void testNullAgentBuilderIsSafe() { @Test public void testAgentBuilderHasDefaultIgnoreMatcher() { ElementMatcher ignoreMatcher = InterceptionInstaller.createIgnoreMatcher(ElementMatchers.none()); - InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); AgentBuilder agentBuilder = Mockito.mock(AgentBuilder.class); - Supplier original = interceptionInstaller.setAgentBuilderFactory(()->agentBuilder); + InterceptionInstaller interceptionInstaller = new InterceptionInstaller(()->agentBuilder); interceptionInstaller.install(Mockito.mock(Instrumentation.class), new HashSet<>(Arrays.asList((a)->a)), new AgentConfig(null), ElementMatchers.none()); Mockito.verify(agentBuilder).ignore(Mockito.eq(ignoreMatcher)); - interceptionInstaller.setAgentBuilderFactory(original); } @Test public void testAgentBuilderNotHasListenerWhenNotVerbose() { - InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); - Supplier original = interceptionInstaller.setAgentBuilderFactory(factory); + InterceptionInstaller interceptionInstaller = new InterceptionInstaller(factory); interceptionInstaller.install(Mockito.mock(Instrumentation.class), new HashSet<>(Arrays.asList((a)->a)), new AgentConfig(null), ElementMatchers.none()); Mockito.verify(factory.agentBuilder, Mockito.never()).with(Mockito.any(AgentBuilder.Listener.class)); - interceptionInstaller.setAgentBuilderFactory(original); } @Test public void testAgentBuilderHasListenerWhenVerbose() { - InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); - Supplier original = interceptionInstaller.setAgentBuilderFactory(factory); + InterceptionInstaller interceptionInstaller = new InterceptionInstaller(factory); AgentConfig agentConfig = new AgentConfigParser().parseCommandLine("extraverbose"); interceptionInstaller.install(Mockito.mock(Instrumentation.class), new HashSet<>(Arrays.asList((a)->a)), agentConfig, ElementMatchers.none()); Mockito.verify(factory.agentBuilder).with(Mockito.any(AgentBuilder.Listener.class)); - interceptionInstaller.setAgentBuilderFactory(original); } @Test public void testAgentBuilderIsInstalledOnInstrumentation() { - InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); - Supplier original = interceptionInstaller.setAgentBuilderFactory(factory); + InterceptionInstaller interceptionInstaller = new InterceptionInstaller(factory); Instrumentation instrumentation = Mockito.mock(Instrumentation.class); interceptionInstaller.install(instrumentation, new HashSet<>(Arrays.asList((a)->a)), new AgentConfig(null), ElementMatchers.none()); Mockito.verify(factory.agentBuilder).installOn(instrumentation); - interceptionInstaller.setAgentBuilderFactory(original); } @Test public void testInstallablesInstallMethodCalled() { - InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); - Supplier original = interceptionInstaller.setAgentBuilderFactory(factory); + InterceptionInstaller interceptionInstaller = new InterceptionInstaller(factory); Instrumentation instrumentation = Mockito.mock(Instrumentation.class); Set installables = new HashSet<>(Arrays.asList(Mockito.mock(Installable.class))); interceptionInstaller.install(instrumentation, installables, new AgentConfig(null), ElementMatchers.none()); for (Installable installable: installables) { Mockito.verify(installable).install(factory.agentBuilder); } - interceptionInstaller.setAgentBuilderFactory(original); } @Test public void testInstallWorksWithACollectionOfInstallables() { - InterceptionInstaller interceptionInstaller = Mockito.spy(InterceptionInstaller.getInstance()); + AgentBuilder agentBuilder = Mockito.mock(AgentBuilder.class); + InterceptionInstaller interceptionInstaller = Mockito.spy(new InterceptionInstaller(()->agentBuilder)); Instrumentation instrumentation = Mockito.mock(Instrumentation.class); Installable installable_a = Mockito.mock(Installable.class); @@ -152,10 +144,9 @@ public void testInstallWorksWithACollectionOfInstallables() { @Test public void testDefaultAgentBuilderTransformerDoesNothing() { - InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); - MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); - interceptionInstaller.setAgentBuilderFactory(factory); + InterceptionInstaller interceptionInstaller = new InterceptionInstaller(factory); + AgentBuilder builder = factory.get(); AgentConfig agentConfig = Mockito.spy(new AgentConfig(null)); @@ -164,7 +155,6 @@ public void testDefaultAgentBuilderTransformerDoesNothing() { //spy on the real default transformer BiFunction agentBuilderTransformer = Mockito.spy(agentConfig.getAgentBuilderTransformer()); - interceptionInstaller.setAgentBuilderFactory(factory); //return the spied instance of the default transformer instead. Mockito.when(agentConfig.getAgentBuilderTransformer()).thenReturn(agentBuilderTransformer); @@ -185,10 +175,8 @@ public void testDefaultAgentBuilderTransformerDoesNothing() { @Test public void testNonDefaultAgentBuilderTransformerReturningSameInstance(){ - InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); - MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); - interceptionInstaller.setAgentBuilderFactory(factory); + InterceptionInstaller interceptionInstaller = new InterceptionInstaller(factory); AgentBuilder builder = factory.get(); AgentConfig agentConfig = Mockito.spy(new AgentConfig(null)); @@ -220,10 +208,8 @@ public AgentBuilder apply(AgentBuilder agentBuilder, Installable installable) { @Test public void testNonDefaultAgentBuilderTransformerReturningDifferentInstance(){ - InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); - MockAgentBuilderFactory factory = new MockAgentBuilderFactory(); - interceptionInstaller.setAgentBuilderFactory(factory); + InterceptionInstaller interceptionInstaller = new InterceptionInstaller(factory); AgentBuilder originalBuilder = factory.get(); AgentBuilder differentBuilder = Mockito.mock(AgentBuilder.class); From 02ef51edd455eea2be47496f781a34f60df94aba Mon Sep 17 00:00:00 2001 From: Connell Date: Fri, 24 Jul 2020 13:30:18 -0700 Subject: [PATCH 30/45] Make the configured AgentBuilder transformer non-static. Fix surrounding code to use instance rather than static methods. Fix tests which were leaving it in an uncertain state. --- .../preprocess/cli/Driver.java | 2 +- .../preprocess/cli/PreprocessConfig.java | 3 +++ .../cli/PreprocessConfigParser.java | 3 ++- .../loaders/agents/DiscoAgentLoader.java | 19 +++++++++++++++---- .../loaders/agents/DiscoAgentLoaderTest.java | 17 +++++++++++------ .../disco/agent/config/AgentConfig.java | 8 ++++---- .../disco/agent/config/AgentConfigTest.java | 4 ++-- 7 files changed, 38 insertions(+), 18 deletions(-) diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java index ed322d9..b0c6293 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java @@ -32,7 +32,7 @@ public static void main(String[] args) { } ModuleTransformer.builder() - .agentLoader(new DiscoAgentLoader(config.getAgentPath())) + .agentLoader(new DiscoAgentLoader(config.getAgentPath(), config.getCoreAgentConfig())) .jarLoader(new JarModuleLoader(config.getJarPaths())) .suffix(config.getSuffix()) .logLevel(config.getLogLevel()) diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java index 2eab084..6ec2311 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java @@ -19,6 +19,7 @@ import lombok.Getter; import lombok.Singular; import org.apache.logging.log4j.Level; +import software.amazon.disco.agent.config.AgentConfig; import java.util.List; @@ -36,4 +37,6 @@ public class PreprocessConfig { private final String suffix; private final Level logLevel; private final String serializationJarPath; + + private final AgentConfig coreAgentConfig; } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java index 9ffe73e..14fa32b 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java @@ -19,6 +19,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.Level; +import software.amazon.disco.agent.config.AgentConfig; import java.util.HashMap; import java.util.Map; @@ -49,7 +50,7 @@ public PreprocessConfig parseCommandLine(String[] args) { } setupAcceptedFlags(); - PreprocessConfig.PreprocessConfigBuilder builder = PreprocessConfig.builder(); + PreprocessConfig.PreprocessConfigBuilder builder = PreprocessConfig.builder().coreAgentConfig(new AgentConfig(null)); OptionToMatch flagBeingMatched = null; diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java index 06b4799..ebfea94 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java @@ -15,6 +15,7 @@ package software.amazon.disco.instrumentation.preprocess.loaders.agents; +import net.bytebuddy.agent.builder.AgentBuilder; import software.amazon.disco.agent.config.AgentConfig; import software.amazon.disco.agent.inject.Injector; import software.amazon.disco.agent.interception.Installable; @@ -23,6 +24,7 @@ import java.io.File; import java.lang.instrument.Instrumentation; +import java.util.function.BiFunction; /** * Agent loader used to dynamically load a Java Agent at runtime by calling the @@ -30,17 +32,19 @@ */ public class DiscoAgentLoader implements AgentLoader { protected String path; + private final AgentConfig agentConfig; /** * Constructor * * @param path path of the agent to be loaded */ - public DiscoAgentLoader(final String path) { + public DiscoAgentLoader(final String path, AgentConfig agentConfig) { if (path == null) { throw new NoPathProvidedException(); } this.path = path; + this.agentConfig = agentConfig; } /** @@ -52,8 +56,7 @@ public void loadAgent() { final Instrumentation instrumentation = Injector.createInstrumentation(); Injector.addToBootstrapClasspath(instrumentation, new File(path)); - AgentConfig.setAgentBuilderTransformer( - (agentBuilder, installable) -> agentBuilder.with(new TransformationListener(uuidGenerate(installable)))); + agentConfig.setAgentBuilderTransformer(getAgentBuilderTransformer()); Injector.loadAgent(instrumentation, path, null); } @@ -64,8 +67,16 @@ public void loadAgent() { * @param installable an Installable that will have a TransformationListener installed on. * @return a uuid that identifies the Installable passed in. */ - private String uuidGenerate(Installable installable) { + private static String uuidGenerate(Installable installable) { return "mock uuid"; //TODO } + + /** + * Access the AgentBuilder transformer that DiscoAgentLoader will use. Package-private for tests. + * @return an AgentBuilder transformer suitable for the code InterceptionInstaller. + */ + static BiFunction getAgentBuilderTransformer() { + return (agentBuilder, installable) -> agentBuilder.with(new TransformationListener(uuidGenerate(installable))); + } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java index c726c7f..1a123d0 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java @@ -27,18 +27,23 @@ public class DiscoAgentLoaderTest { @Test(expected = NoPathProvidedException.class) public void testConstructorFailOnNullPaths() throws NoPathProvidedException { - new DiscoAgentLoader(null); + new DiscoAgentLoader(null, null); } @Test public void testLoadAgentCallsSetAgentBuilderTransformer(){ - DiscoAgentLoader loader = Mockito.spy(new DiscoAgentLoader("a path")); - AgentBuilder builder = Mockito.mock(AgentBuilder.class); - Installable installable = Mockito.mock(Installable.class); - + AgentConfig agentConfig = new AgentConfig(null); + DiscoAgentLoader loader = new DiscoAgentLoader("a path", agentConfig); loader.loadAgent(); - new AgentConfig(null).getAgentBuilderTransformer().apply(builder, installable); + Assert.assertEquals(agentConfig.getAgentBuilderTransformer(), DiscoAgentLoader.getAgentBuilderTransformer()); + + } + @Test + public void testAgentBuilderTransformerTransforms() { + AgentBuilder builder = Mockito.mock(AgentBuilder.class); + Installable installable = Mockito.mock(Installable.class); + DiscoAgentLoader.getAgentBuilderTransformer().apply(builder, installable); Mockito.verify(builder).with(Mockito.any(TransformationListener.class)); } } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java index 3470460..5214d88 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java @@ -25,7 +25,7 @@ * Holds agent configuration parsed during bootstrap. */ public class AgentConfig { - private static BiFunction agentBuilderTransformer = new NoOpAgentBuilderTransformer(); + private BiFunction agentBuilderTransformer = new NoOpAgentBuilderTransformer(); private List args; private boolean isRuntimeOnly = false; @@ -60,8 +60,8 @@ public AgentBuilder apply(AgentBuilder agentBuilder, Installable installable) { * * @param agentBuilderTransformer the AgentBuilder Transformer to be applied to an AgentBuilder */ - public static void setAgentBuilderTransformer(BiFunction agentBuilderTransformer) { - AgentConfig.agentBuilderTransformer = agentBuilderTransformer == null ? new NoOpAgentBuilderTransformer() : agentBuilderTransformer; + public void setAgentBuilderTransformer(BiFunction agentBuilderTransformer) { + this.agentBuilderTransformer = agentBuilderTransformer == null ? new NoOpAgentBuilderTransformer() : agentBuilderTransformer; } /** @@ -70,7 +70,7 @@ public static void setAgentBuilderTransformer(BiFunction getAgentBuilderTransformer() { - return AgentConfig.agentBuilderTransformer; + return agentBuilderTransformer; } /** diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigTest.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigTest.java index a11cd7a..0b101cd 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigTest.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/config/AgentConfigTest.java @@ -21,14 +21,14 @@ public void testSetNonDefaultAgentBuilderTransformer() { BiFunction defaultTransformer = config.getAgentBuilderTransformer(); BiFunction mockTransformer = Mockito.mock(BiFunction.class); - AgentConfig.setAgentBuilderTransformer(mockTransformer); + config.setAgentBuilderTransformer(mockTransformer); Assert.assertNotEquals(defaultTransformer, config.getAgentBuilderTransformer()); } @Test public void testSetNullAgentBuilderTransformerResetsToDefaultTransformer(){ - AgentConfig.setAgentBuilderTransformer(null); + config.setAgentBuilderTransformer(null); Assert.assertNotNull(config.getAgentBuilderTransformer()); } From 0ec935a6078a01c3941d5077ad136945bd1ed45c Mon Sep 17 00:00:00 2001 From: Hongbo Liu Date: Fri, 24 Jul 2020 13:14:40 -0400 Subject: [PATCH 31/45] fixed preprocess lib build shading rules and moved agent classpath injection to main --- .../build.gradle.kts | 8 +++++--- .../disco/instrumentation/preprocess/cli/Driver.java | 12 +++++++++++- .../preprocess/export/JarModuleExportStrategy.java | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/disco-java-agent-instrumentation-preprocess/build.gradle.kts b/disco-java-agent-instrumentation-preprocess/build.gradle.kts index 0f9753c..8c0a604 100644 --- a/disco-java-agent-instrumentation-preprocess/build.gradle.kts +++ b/disco-java-agent-instrumentation-preprocess/build.gradle.kts @@ -15,12 +15,14 @@ plugins { id("io.freefair.lombok") version "5.1.0" + id("com.github.johnrengelman.shadow") } dependencies { - implementation(project(":disco-java-agent:disco-java-agent-core")) - implementation(project(":disco-java-agent:disco-java-agent-api")) + compileOnly(project(":disco-java-agent:disco-java-agent-core")) + compileOnly(project(":disco-java-agent:disco-java-agent-api")) implementation(project(":disco-java-agent:disco-java-agent-inject-api")) - implementation("org.apache.logging.log4j", "log4j-core", "2.13.3") + testImplementation(project(":disco-java-agent:disco-java-agent-core")) + testImplementation(project(":disco-java-agent:disco-java-agent-api")) } \ No newline at end of file diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java index b0c6293..115dac5 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java @@ -15,10 +15,14 @@ package software.amazon.disco.instrumentation.preprocess.cli; +import software.amazon.disco.agent.inject.Injector; import software.amazon.disco.instrumentation.preprocess.instrumentation.ModuleTransformer; import software.amazon.disco.instrumentation.preprocess.loaders.agents.DiscoAgentLoader; import software.amazon.disco.instrumentation.preprocess.loaders.modules.JarModuleLoader; +import java.io.File; +import java.lang.instrument.Instrumentation; + /** * Entry point of the library. A {@link ModuleTransformer} instance is being created to orchestrate the instrumentation * process of all packages supplied. @@ -27,10 +31,16 @@ public class Driver { public static void main(String[] args) { final PreprocessConfig config = new PreprocessConfigParser().parseCommandLine(args); - if(config == null){ + if (config == null) { System.exit(1); } + final Instrumentation instrumentation = Injector.createInstrumentation(); + + // inject the agent jar into the classpath as earlier as possible to avoid ClassNotFound exception when resolving + // types imported from libraries such as ByteBuddy shaded in the agent JAR + Injector.addToBootstrapClasspath(instrumentation, new File(config.getAgentPath())); + ModuleTransformer.builder() .agentLoader(new DiscoAgentLoader(config.getAgentPath(), config.getCoreAgentConfig())) .jarLoader(new JarModuleLoader(config.getJarPaths())) diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java index b5218af..ebce64e 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java @@ -95,7 +95,7 @@ protected File createTempFile(final ModuleInfo moduleInfo) { tempDir = Files.createTempDirectory("disco"); } - return Files.createTempFile(tempDir, moduleInfo.getJarFile().getName(), null).toFile(); + return Files.createTempFile(tempDir, moduleInfo.getFile().getName(), null).toFile(); } catch (IOException e) { throw new ModuleExportException("Failed to create temp Jar file", e); } From ef246b72f21eb995ea52bb340ecaee05c9eacbfe Mon Sep 17 00:00:00 2001 From: Hongbo Liu Date: Mon, 27 Jul 2020 14:01:38 -0400 Subject: [PATCH 32/45] Implemented hook in DiscoAgentTemplate to set an AgentConfigFactory --- .../preprocess/cli/Driver.java | 7 +- .../preprocess/cli/PreprocessConfig.java | 5 +- .../cli/PreprocessConfigParser.java | 15 ++- .../InvalidConfigEntryException.java | 34 +++++ ...ption.java => NoAgentToLoadException.java} | 11 +- .../instrumentation/ModuleTransformer.java | 48 ++++--- .../loaders/agents/AgentLoader.java | 8 +- .../loaders/agents/DiscoAgentLoader.java | 79 ++++++++---- .../loaders/modules/JarModuleLoader.java | 30 ++--- .../loaders/modules/ModuleLoader.java | 6 +- .../cli/PreprocessConfigParserTest.java | 24 +++- .../ModuleTransformerTest.java | 68 +++++----- .../loaders/agents/DiscoAgentLoaderTest.java | 119 +++++++++++++++--- .../loaders/modules/JarModuleLoaderTest.java | 62 ++++----- .../disco/agent/DiscoAgentTemplate.java | 29 ++++- .../disco/agent/config/AgentConfig.java | 3 + .../disco/agent/DiscoAgentTemplateTests.java | 25 ++++ 17 files changed, 395 insertions(+), 178 deletions(-) create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InvalidConfigEntryException.java rename disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/{NoPathProvidedException.java => NoAgentToLoadException.java} (69%) diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java index 115dac5..d0c4f09 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java @@ -42,10 +42,9 @@ public static void main(String[] args) { Injector.addToBootstrapClasspath(instrumentation, new File(config.getAgentPath())); ModuleTransformer.builder() - .agentLoader(new DiscoAgentLoader(config.getAgentPath(), config.getCoreAgentConfig())) - .jarLoader(new JarModuleLoader(config.getJarPaths())) - .suffix(config.getSuffix()) - .logLevel(config.getLogLevel()) + .agentLoader(new DiscoAgentLoader()) + .jarLoader(new JarModuleLoader()) + .config(config) .build() .transform(); } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java index 6ec2311..bc9037b 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java @@ -21,7 +21,7 @@ import org.apache.logging.log4j.Level; import software.amazon.disco.agent.config.AgentConfig; -import java.util.List; +import java.util.Set; /** * Container for the config created from the command line args @@ -30,13 +30,14 @@ @Getter public class PreprocessConfig { @Singular - private final List jarPaths; + private final Set jarPaths; private final String outputDir; private final String agentPath; private final String suffix; private final Level logLevel; private final String serializationJarPath; + private final String javaVersion; private final AgentConfig coreAgentConfig; } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java index 14fa32b..d6820c2 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java @@ -50,7 +50,7 @@ public PreprocessConfig parseCommandLine(String[] args) { } setupAcceptedFlags(); - PreprocessConfig.PreprocessConfigBuilder builder = PreprocessConfig.builder().coreAgentConfig(new AgentConfig(null)); + PreprocessConfig.PreprocessConfigBuilder builder = PreprocessConfig.builder(); OptionToMatch flagBeingMatched = null; @@ -74,7 +74,7 @@ public PreprocessConfig parseCommandLine(String[] args) { } else { // previous flag still expecting an argument but another flag is discovered if (ACCEPTED_FLAGS.containsKey(argLowered) && !flagBeingMatched.isMatched()) { - System.err.println("Flag: [" + flagBeingMatched + "] requires an argument"); + System.err.println("Flag: [" + flagBeingMatched.getFlag() + "] requires an argument"); return null; } @@ -98,8 +98,8 @@ public PreprocessConfig parseCommandLine(String[] args) { } // the last flag discovered is missing its arg - if (flagBeingMatched != null) { - System.err.println("Flag: [" + flagBeingMatched + "] requires an argument"); + if (flagBeingMatched != null && !flagBeingMatched.isMatched) { + System.err.println("Flag: [" + flagBeingMatched.getFlag() + "] requires an argument"); return null; } @@ -119,12 +119,14 @@ protected void setupAcceptedFlags() { ACCEPTED_FLAGS.put("--agentpath", new OptionToMatch("--agentpath", true, false)); ACCEPTED_FLAGS.put("--serializationpath", new OptionToMatch("--serializationpath", true, false)); ACCEPTED_FLAGS.put("--suffix", new OptionToMatch("--suffix", true, false)); + ACCEPTED_FLAGS.put("--javaversion", new OptionToMatch("--javaversion", true, false)); ACCEPTED_FLAGS.put("-out", new OptionToMatch("-out", true, false)); ACCEPTED_FLAGS.put("-jps", new OptionToMatch("-jps", true, true)); ACCEPTED_FLAGS.put("-ap", new OptionToMatch("-ap", true, false)); ACCEPTED_FLAGS.put("-sp", new OptionToMatch("-sp", true, false)); ACCEPTED_FLAGS.put("-suf", new OptionToMatch("-suf", true, false)); + ACCEPTED_FLAGS.put("-jv", new OptionToMatch("-jv", true, false)); } /** @@ -139,6 +141,7 @@ protected void printHelpText() { + "\t\t --serializationPath | -sp \n" + "\t\t --agentPath | -ap \n" + "\t\t --suffix | -suf \n" + + "\t\t --javaversion | -jv \n" + "\t\t --verbose Set the log level to log everything.\n" + "\t\t --silent Disable logging to the console.\n\n" + "The default behavior of the library will replace the original package scheduled for instrumentation if NO destination AND suffix are supplied.\n" @@ -180,6 +183,10 @@ protected OptionToMatch matchArgWithFlag(OptionToMatch option, String argument, case "--suffix": builder.suffix(argument); break; + case "-jv": + case "--javaversion": + builder.javaVersion(argument); + break; default: // will never be invoked since flags are already validated. } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InvalidConfigEntryException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InvalidConfigEntryException.java new file mode 100644 index 0000000..0473b45 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InvalidConfigEntryException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when encountering an entry from {@link software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig} + * that is invalid. + */ +public class InvalidConfigEntryException extends RuntimeException { + /** + * Constructor that calls its parent with a fixed message + * + * @param configEntry config entry that is invalid + * @pram t cause of the error + */ + public InvalidConfigEntryException(String configEntry, Throwable t) { + super(PreprocessConstants.MESSAGE_PREFIX + "Invalid configuration entry: " + configEntry, t); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoPathProvidedException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoAgentToLoadException.java similarity index 69% rename from disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoPathProvidedException.java rename to disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoAgentToLoadException.java index ae48155..631b4a4 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoPathProvidedException.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/NoAgentToLoadException.java @@ -15,19 +15,16 @@ package software.amazon.disco.instrumentation.preprocess.exceptions; -import software.amazon.disco.instrumentation.preprocess.loaders.agents.AgentLoader; -import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleLoader; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; /** - * Exception thrown when initializing {@link ModuleLoader} - * or {@link AgentLoader} with no path provided. + * Exception thrown when no path to an agent is provided in the {@link software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig config} */ -public class NoPathProvidedException extends RuntimeException { +public class NoAgentToLoadException extends RuntimeException { /** * Constructor invoking the parent constructor with a fixed error message */ - public NoPathProvidedException() { - super(PreprocessConstants.MESSAGE_PREFIX + "No path provided to load agent or package"); + public NoAgentToLoadException() { + super(PreprocessConstants.MESSAGE_PREFIX + "No path provided to load agent"); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java index 3617f01..d2357df 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java @@ -21,7 +21,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator; +import software.amazon.disco.agent.inject.Injector; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.AgentLoaderNotProvidedException; +import software.amazon.disco.instrumentation.preprocess.exceptions.InvalidConfigEntryException; import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleLoaderNotProvidedException; import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.loaders.agents.AgentLoader; @@ -45,31 +48,40 @@ public class ModuleTransformer { private final ModuleLoader jarLoader; private final AgentLoader agentLoader; - private final String suffix; - private final Level logLevel; + private final PreprocessConfig config; /** - * This method initiates the transformation process of all packages found under the provided paths. + * This method initiates the transformation process of all packages found under the provided paths. All Runtime exceptions + * thrown by the library are handled in this method. A detailed error message along with any available Cause will be logged + * and trigger the program to exit with status 1 */ public void transform() { - if (logLevel == null) { - Configurator.setRootLevel(Level.INFO); - }else{ - Configurator.setRootLevel(logLevel); - } + try { + if (config == null) {throw new InvalidConfigEntryException("No configuration provided", null);} - if (agentLoader == null) throw new AgentLoaderNotProvidedException(); + if (config.getLogLevel() == null) { + Configurator.setRootLevel(Level.INFO); + } else { + Configurator.setRootLevel(config.getLogLevel()); + } - agentLoader.loadAgent(); + if (agentLoader == null) {throw new AgentLoaderNotProvidedException();} + if (jarLoader == null) {throw new ModuleLoaderNotProvidedException();} - if (jarLoader == null) { - throw new ModuleLoaderNotProvidedException(); - } + agentLoader.loadAgent(config, Injector.createInstrumentation()); - // Apply instrumentation on all jars - for (final ModuleInfo info : jarLoader.loadPackages()) { - applyInstrumentation(info); - //todo: store serialized instrumentation state to target jar + if (jarLoader == null) { + throw new ModuleLoaderNotProvidedException(); + } + + // Apply instrumentation on all jars + for (final ModuleInfo info : jarLoader.loadPackages(config)) { + applyInstrumentation(info); + //todo: store serialized instrumentation state to target jar + } + } catch (RuntimeException e) { + log.error(e); + System.exit(1); } } @@ -89,7 +101,7 @@ protected void applyInstrumentation(final ModuleInfo moduleInfo) { } } - moduleInfo.getExportStrategy().export(moduleInfo, getInstrumentedClasses(), suffix); + moduleInfo.getExportStrategy().export(moduleInfo, getInstrumentedClasses(), config.getSuffix()); // empty the map in preparation for transforming another package getInstrumentedClasses().clear(); diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/AgentLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/AgentLoader.java index f83ebcb..b58c901 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/AgentLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/AgentLoader.java @@ -16,14 +16,18 @@ package software.amazon.disco.instrumentation.preprocess.loaders.agents; import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; + +import java.lang.instrument.Instrumentation; /** * Agent loader interface that loads Java Agents and {@link Installable} */ public interface AgentLoader { - /** * load and install the agent dynamically at runtime. + * @param config a PreprocessConfig containing information to perform module instrumentation + * @param instrumentation instrumentation instance that will be used to load the agent */ - void loadAgent(); + void loadAgent(PreprocessConfig config, Instrumentation instrumentation); } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java index ebfea94..fa43aa0 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java @@ -15,14 +15,21 @@ package software.amazon.disco.instrumentation.preprocess.loaders.agents; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.ClassFileVersion; import net.bytebuddy.agent.builder.AgentBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.disco.agent.DiscoAgentTemplate; import software.amazon.disco.agent.config.AgentConfig; import software.amazon.disco.agent.inject.Injector; import software.amazon.disco.agent.interception.Installable; -import software.amazon.disco.instrumentation.preprocess.exceptions.NoPathProvidedException; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; +import software.amazon.disco.instrumentation.preprocess.exceptions.InvalidConfigEntryException; +import software.amazon.disco.instrumentation.preprocess.exceptions.NoAgentToLoadException; import software.amazon.disco.instrumentation.preprocess.instrumentation.TransformationListener; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; -import java.io.File; import java.lang.instrument.Instrumentation; import java.util.function.BiFunction; @@ -31,34 +38,32 @@ * {@link Injector} api. */ public class DiscoAgentLoader implements AgentLoader { - protected String path; - private final AgentConfig agentConfig; - - /** - * Constructor - * - * @param path path of the agent to be loaded - */ - public DiscoAgentLoader(final String path, AgentConfig agentConfig) { - if (path == null) { - throw new NoPathProvidedException(); - } - this.path = path; - this.agentConfig = agentConfig; - } + private final static Logger log = LogManager.getLogger(DiscoAgentLoader.class); /** * {@inheritDoc} + * * Install an agent by directly invoking the {@link Injector} api. */ @Override - public void loadAgent() { - final Instrumentation instrumentation = Injector.createInstrumentation(); - Injector.addToBootstrapClasspath(instrumentation, new File(path)); + public void loadAgent(final PreprocessConfig config, Instrumentation instrumentation) { + if (config == null || config.getAgentPath() == null) { + throw new NoAgentToLoadException(); + } + + instrumentation = instrumentation == null ? Injector.createInstrumentation() : instrumentation; - agentConfig.setAgentBuilderTransformer(getAgentBuilderTransformer()); + final ClassFileVersion version = parseClassFileVersionFromConfig(config); - Injector.loadAgent(instrumentation, path, null); + DiscoAgentTemplate.setAgentConfigFactory(() -> { + //todo if we want to pass on any args from the tool to Core, pass them here. + final AgentConfig coreConfig = new AgentConfig(null); + + coreConfig.setAgentBuilderTransformer(getAgentBuilderTransformer(version)); + return coreConfig; + }); + + Injector.loadAgent(instrumentation, config.getAgentPath(), null); } /** @@ -72,11 +77,35 @@ private static String uuidGenerate(Installable installable) { } /** - * Access the AgentBuilder transformer that DiscoAgentLoader will use. Package-private for tests. + * Parses a java version supplied by the {@link PreprocessConfig} file. Default is java 8 if not specified. + * + * @param config a PreprocessConfig containing information to perform module instrumentation + * @return a parsed instance of ClassFileVersion + * @throws InvalidConfigEntryException if supplied config value is invalid + */ + protected static ClassFileVersion parseClassFileVersionFromConfig(PreprocessConfig config) { + try { + if (config.getJavaVersion() == null) { + log.info(PreprocessConstants.MESSAGE_PREFIX + "Java version to compile transformed classes not specified, set to Java 8 by default"); + return ClassFileVersion.ofJavaVersion(8); + } else { + return ClassFileVersion.ofJavaVersion(Integer.parseInt(config.getJavaVersion())); + } + } catch (IllegalArgumentException e) { + throw new InvalidConfigEntryException("java version: " + config.getJavaVersion(), e); + } + } + + /** + * Returns an AgentBuilder transformer that DiscoAgentTemplate will use to transform an AgentBuilder. + * + * @param version java version used to compile the transformed classes * @return an AgentBuilder transformer suitable for the code InterceptionInstaller. */ - static BiFunction getAgentBuilderTransformer() { - return (agentBuilder, installable) -> agentBuilder.with(new TransformationListener(uuidGenerate(installable))); + private BiFunction getAgentBuilderTransformer(ClassFileVersion version) { + return (agentBuilder, installable) -> agentBuilder + .with(new ByteBuddy(version)) + .with(new TransformationListener(uuidGenerate(installable))); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java index f09d4cc..9cd3368 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java @@ -19,8 +19,8 @@ import software.amazon.disco.agent.inject.Injector; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; -import software.amazon.disco.instrumentation.preprocess.exceptions.NoPathProvidedException; import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; @@ -30,9 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -42,32 +40,22 @@ public class JarModuleLoader implements ModuleLoader { private static final Logger log = LogManager.getLogger(JarModuleLoader.class); - @Getter - private final Set paths; - @Getter private final ModuleExportStrategy strategy; /** - * Constructor that sets default package export strategy as {@link JarModuleExportStrategy} - * - * @param paths list of paths to load Jar files + * Default constructor that sets {@link #strategy} to {@link JarModuleExportStrategy} */ - public JarModuleLoader(final List paths) throws NoPathProvidedException { - if (paths == null || paths.size() == 0) throw new NoPathProvidedException(); - this.paths = new HashSet<>(paths); + public JarModuleLoader() { this.strategy = new JarModuleExportStrategy(); } /** - * Constructor + * Constructor accepting a custom export strategy * * @param strategy {@link ModuleExportStrategy strategy} for exporting transformed classes under this path. Default strategy is {@link JarModuleExportStrategy} - * @param paths list of paths to load Jar files */ - public JarModuleLoader(final ModuleExportStrategy strategy, final List paths) throws NoPathProvidedException { - if (paths == null || paths.size() == 0) throw new NoPathProvidedException(); - this.paths = new HashSet<>(paths); + public JarModuleLoader(final ModuleExportStrategy strategy) { this.strategy = strategy; } @@ -75,10 +63,14 @@ public JarModuleLoader(final ModuleExportStrategy strategy, final List p * {@inheritDoc} */ @Override - public List loadPackages() { + public List loadPackages(PreprocessConfig config) { + if (config == null || config.getJarPaths() == null) { + throw new NoModuleToInstrumentException(); + } + final List packageEntries = new ArrayList<>(); - for (String path : paths) { + for (String path : config.getJarPaths()) { for (File file : discoverFilesInPath(path)) { final ModuleInfo info = loadPackage(file); diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java index 947674b..1893ef8 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java @@ -15,6 +15,8 @@ package software.amazon.disco.instrumentation.preprocess.loaders.modules; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; + import java.util.List; /** @@ -25,7 +27,9 @@ public interface ModuleLoader { * Loads all the modules found under the paths specified and aggregate them into a single list of {@link ModuleInfo}. * Names of all the classes within each package are discovered and stored inside {@link ModuleInfo}. * + * @param config a PreprocessConfig containing information to perform module instrumentation + * * @return list of {@link ModuleInfo} loaded by this package loader. Empty if no modules found. */ - List loadPackages(); + List loadPackages(PreprocessConfig config); } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java index fd6996c..a9fca6d 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java @@ -22,6 +22,7 @@ import org.mockito.Mockito; import java.util.Arrays; +import java.util.HashSet; public class PreprocessConfigParserTest { String outputDir = "/d"; @@ -86,35 +87,39 @@ public void parseCommandLineWorksWithFullCommandNamesAndReturnsConfigFile() { "--jarpaths", "/d1", "/d2", "/d3", "--serializationpath", serialization, "--agentPath", agent, - "--suffix", suffix + "--suffix", suffix, + "--javaversion", "11" }; PreprocessConfig config = preprocessConfigParser.parseCommandLine(args); Assert.assertEquals(outputDir, config.getOutputDir()); Assert.assertEquals(serialization, config.getSerializationJarPath()); - Assert.assertEquals(Arrays.asList("/d1", "/d2", "/d3"), config.getJarPaths()); + Assert.assertEquals(new HashSet<>(Arrays.asList("/d1", "/d2", "/d3")), config.getJarPaths()); Assert.assertEquals(agent, config.getAgentPath()); Assert.assertEquals(suffix, config.getSuffix()); + Assert.assertEquals("11", config.getJavaVersion()); } @Test - public void parseCommandLineWorksWithShortHandCommandNamesAndReturnsConfigFile() { + public void testParseCommandLineWorksWithShortHandCommandNamesAndReturnsConfigFile() { String[] args = new String[]{ "-out", outputDir, "-jps", "/d1", "/d2", "/d3", "-sp", serialization, "-ap", agent, - "-suf", suffix + "-suf", suffix, + "-jv", "11" }; PreprocessConfig config = preprocessConfigParser.parseCommandLine(args); Assert.assertEquals(outputDir, config.getOutputDir()); Assert.assertEquals(serialization, config.getSerializationJarPath()); - Assert.assertEquals(Arrays.asList("/d1", "/d2", "/d3"), config.getJarPaths()); + Assert.assertEquals(new HashSet<>(Arrays.asList("/d1", "/d2", "/d3")), config.getJarPaths()); Assert.assertEquals(agent, config.getAgentPath()); Assert.assertEquals(suffix, config.getSuffix()); + Assert.assertEquals("11", config.getJavaVersion()); } @Test @@ -128,4 +133,13 @@ public void parseCommandLineWorkWithHelpFlag() { spyParser.parseCommandLine(new String[]{"--verbose", "--help"}); Mockito.verify(spyParser, Mockito.never()).printHelpText(); } + + @Test + public void testParseCommandLineWithDuplicatePaths(){ + String[] args = new String[]{ + "-jps", "/d1", "/d1", "/d2", + }; + PreprocessConfig config = preprocessConfigParser.parseCommandLine(args); + Assert.assertEquals(2, config.getJarPaths().size()); + } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java index 05970c3..a54ac0f 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java @@ -24,14 +24,14 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -import software.amazon.disco.instrumentation.preprocess.exceptions.AgentLoaderNotProvidedException; -import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleLoaderNotProvidedException; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.loaders.agents.DiscoAgentLoader; import software.amazon.disco.instrumentation.preprocess.loaders.modules.JarModuleLoader; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; import software.amazon.disco.instrumentation.preprocess.util.MockEntities; +import java.lang.instrument.Instrumentation; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -49,19 +49,27 @@ public class ModuleTransformerTest { @Mock JarModuleLoader mockJarPackageLoader; + PreprocessConfig config; List moduleInfos; @Before public void before() { + config = PreprocessConfig.builder() + .agentPath("a path") + .jarPath("a path") + .suffix(PACKAGE_SUFFIX) + .build(); + spyTransformer = Mockito.spy( ModuleTransformer.builder() .jarLoader(mockJarPackageLoader) .agentLoader(mockAgentLoader) - .suffix(PACKAGE_SUFFIX) + .config(config) .build() ); - Mockito.doReturn(Arrays.asList(MockEntities.makeMockPackageInfo())).when(mockJarPackageLoader).loadPackages(); + Mockito.doReturn(Arrays.asList(MockEntities.makeMockPackageInfo())) + .when(mockJarPackageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); moduleInfos = new ArrayList<>(); moduleInfos.add(Mockito.mock(ModuleInfo.class)); @@ -69,67 +77,49 @@ public void before() { } @Test - public void testTransformWorksWithDefaultLogLevel(){ + public void testTransformWorksWithDefaultLogLevel() { spyTransformer.transform(); Assert.assertEquals(LogManager.getLogger().getLevel(), Level.INFO); } @Test - public void testTransformWorksWithVerboseLogLevel(){ - spyTransformer = Mockito.spy( - ModuleTransformer.builder() - .jarLoader(mockJarPackageLoader) - .agentLoader(mockAgentLoader) - .logLevel(Level.TRACE) - .build() - ); - - spyTransformer.transform(); + public void testTransformWorksWithVerboseLogLevel() { + config = PreprocessConfig.builder() + .agentPath("a path") + .jarPath("a path") + .logLevel(Level.TRACE) + .build(); + + ModuleTransformer.builder() + .jarLoader(mockJarPackageLoader) + .agentLoader(mockAgentLoader) + .config(config) + .build().transform(); Assert.assertEquals(Level.TRACE, LogManager.getLogger().getLevel()); } - @Test(expected = AgentLoaderNotProvidedException.class) - public void testTransformFailsWhenNoAgentLoaderProvided(){ - spyTransformer = Mockito.spy( - ModuleTransformer.builder() - .jarLoader(mockJarPackageLoader) - .build() - ); - spyTransformer.transform(); - } - - - @Test(expected = ModuleLoaderNotProvidedException.class) - public void testTransformFailsWhenNoPackageLoaderProvided() { - spyTransformer = Mockito.spy( - ModuleTransformer.builder() - .agentLoader(mockAgentLoader) - .build() - ); - spyTransformer.transform(); - } - @Test public void testTransformWorksAndInvokesLoadAgentAndPackages() { spyTransformer = Mockito.spy( ModuleTransformer.builder() .jarLoader(mockJarPackageLoader) .agentLoader(mockAgentLoader) + .config(config) .build() ); spyTransformer.transform(); - Mockito.verify(mockAgentLoader).loadAgent(); - Mockito.verify(mockJarPackageLoader).loadPackages(); + Mockito.verify(mockAgentLoader).loadAgent(Mockito.any(PreprocessConfig.class), Mockito.any(Instrumentation.class)); + Mockito.verify(mockJarPackageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); } @Test public void testTransformWorksAndInvokesPackageLoader() { spyTransformer.transform(); - Mockito.verify(mockJarPackageLoader).loadPackages(); + Mockito.verify(mockJarPackageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); Mockito.verify(spyTransformer).applyInstrumentation(Mockito.any()); } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java index 1a123d0..4aa5cb1 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java @@ -15,35 +15,124 @@ package software.amazon.disco.instrumentation.preprocess.loaders.agents; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.ClassFileVersion; import net.bytebuddy.agent.builder.AgentBuilder; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import software.amazon.disco.agent.DiscoAgentTemplate; import software.amazon.disco.agent.config.AgentConfig; -import software.amazon.disco.agent.interception.Installable; -import software.amazon.disco.instrumentation.preprocess.exceptions.NoPathProvidedException; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; +import software.amazon.disco.instrumentation.preprocess.exceptions.InvalidConfigEntryException; +import software.amazon.disco.instrumentation.preprocess.exceptions.NoAgentToLoadException; import software.amazon.disco.instrumentation.preprocess.instrumentation.TransformationListener; +import java.io.File; +import java.io.FileOutputStream; +import java.lang.instrument.Instrumentation; +import java.util.function.Supplier; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + public class DiscoAgentLoaderTest { - @Test(expected = NoPathProvidedException.class) - public void testConstructorFailOnNullPaths() throws NoPathProvidedException { - new DiscoAgentLoader(null, null); + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test(expected = NoAgentToLoadException.class) + public void testLoadAgentFailOnNullPaths() throws NoAgentToLoadException { + new DiscoAgentLoader().loadAgent(null, null); } @Test - public void testLoadAgentCallsSetAgentBuilderTransformer(){ - AgentConfig agentConfig = new AgentConfig(null); - DiscoAgentLoader loader = new DiscoAgentLoader("a path", agentConfig); - loader.loadAgent(); - Assert.assertEquals(agentConfig.getAgentBuilderTransformer(), DiscoAgentLoader.getAgentBuilderTransformer()); + public void testParsingJavaVersionWorks(){ + PreprocessConfig config = PreprocessConfig.builder() + .agentPath("path") + .javaVersion("11") + .build(); + ClassFileVersion version = DiscoAgentLoader.parseClassFileVersionFromConfig(config); + Assert.assertEquals(ClassFileVersion.JAVA_V11, version); + + // test if default is set to java 8 + PreprocessConfig anotherConfig = PreprocessConfig.builder() + .agentPath("path") + .build(); + version = DiscoAgentLoader.parseClassFileVersionFromConfig(anotherConfig); + Assert.assertEquals(ClassFileVersion.JAVA_V8, version); + } + @Test(expected = InvalidConfigEntryException.class) + public void testParsingJavaVersionFailsWithInvalidJavaVersion(){ + PreprocessConfig config = PreprocessConfig.builder() + .agentPath("path") + .javaVersion("a version") + .build(); + DiscoAgentLoader.parseClassFileVersionFromConfig(config); } @Test - public void testAgentBuilderTransformerTransforms() { - AgentBuilder builder = Mockito.mock(AgentBuilder.class); - Installable installable = Mockito.mock(Installable.class); - DiscoAgentLoader.getAgentBuilderTransformer().apply(builder, installable); - Mockito.verify(builder).with(Mockito.any(TransformationListener.class)); + public void testLoadAgentRegistersAgentBuilderTransformerAndInstallsAgent() throws Exception { + Instrumentation instrumentation = Mockito.mock(Instrumentation.class); + AgentBuilder agentBuilder = Mockito.mock(AgentBuilder.class); + Mockito.when(agentBuilder.with(Mockito.any(ByteBuddy.class))).thenReturn(agentBuilder); + + File file = createJar("TestJarFile"); + PreprocessConfig config = PreprocessConfig.builder().agentPath(file.getAbsolutePath()).build(); + + Assert.assertNull(DiscoAgentTemplate.getAgentConfigFactory()); + + DiscoAgentLoader loader = Mockito.spy(new DiscoAgentLoader()); + loader.loadAgent(config, instrumentation); + + // check if agentConfigSupplier with an AgentBuilderTransformer is set + Supplier agentConfigSupplier = DiscoAgentTemplate.getAgentConfigFactory(); + Assert.assertNotNull(agentConfigSupplier); + Assert.assertNotNull(agentConfigSupplier.get()); + agentConfigSupplier.get().getAgentBuilderTransformer().apply(agentBuilder, null); + Mockito.verify(agentBuilder).with(Mockito.any(TransformationListener.class)); + + // check if a ByteBuddy instance with the correct java version is being installed using its own + // equals method + Assert.assertEquals(ClassFileVersion.JAVA_V8, DiscoAgentLoader.parseClassFileVersionFromConfig(config)); + ArgumentCaptor byteBuddyArgumentCaptor = ArgumentCaptor.forClass(ByteBuddy.class); + Mockito.verify(agentBuilder).with(byteBuddyArgumentCaptor.capture()); + Assert.assertEquals(new ByteBuddy(ClassFileVersion.JAVA_V8), byteBuddyArgumentCaptor.getValue()); + + // the Injector will invoke addToBootstrapClasspath() which in turn calls the method tested below + ArgumentCaptor jarFileArgumentCaptor = ArgumentCaptor.forClass(JarFile.class); + Mockito.verify(instrumentation).appendToBootstrapClassLoaderSearch(jarFileArgumentCaptor.capture()); + Assert.assertEquals(file.getAbsolutePath(), jarFileArgumentCaptor.getValue().getName()); } + + private File createJar(String name) throws Exception { + File file = temporaryFolder.newFile(name+".jar"); + try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + //write a sentinal file with the same name as the jar, to test if it becomes readable by getResource. + jarOutputStream.putNextEntry(new ZipEntry(name)); + jarOutputStream.write("foobar".getBytes()); + jarOutputStream.closeEntry(); + } + } + return file; + } + +// class MockAgentBuilderTransformer implements BiFunction { +// @Override +// public AgentBuilder apply(AgentBuilder agentBuilder, Installable installable) { +// return agentBuilder +// .with(new ByteBuddy(version)) +// .with(new TransformationListener(uuidGenerate(installable))); +// } +// +// class ByteBuddyTest extends ByteBuddy{ +// public ClassFileVersion getClassFileVersion(){ +// return classFileVersion; +// } +// } +// } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java index b6bc602..2d660d0 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java @@ -22,14 +22,13 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; -import software.amazon.disco.instrumentation.preprocess.exceptions.NoPathProvidedException; import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.util.MockEntities; import java.io.File; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.jar.JarEntry; @@ -43,6 +42,7 @@ public class JarModuleLoaderTest { static final List MOCK_JAR_ENTRIES = MockEntities.makeMockJarEntries(); JarModuleLoader loader; + PreprocessConfig config; @Mock JarFile jarFile; @@ -51,42 +51,39 @@ public class JarModuleLoaderTest { File mockFile; @Before - public void before() throws NoPathProvidedException { - loader = new JarModuleLoader(PATHS); + public void before(){ + config = PreprocessConfig.builder().jarPaths(PATHS).build(); + loader = new JarModuleLoader(); Mockito.when(mockFile.isDirectory()).thenReturn(false); Mockito.when(mockFile.getName()).thenReturn("ATestJar.jar"); } - @Test(expected = NoPathProvidedException.class) - public void testConstructorFailWithEmptyPathList() throws NoPathProvidedException { - new JarModuleLoader(new ArrayList<>()); + @Test(expected = NoModuleToInstrumentException.class) + public void testConstructorFailWithEmptyPathList() { + new JarModuleLoader().loadPackages(config); } - @Test(expected = NoPathProvidedException.class) - public void testConstructorFailWithNullPathList() throws NoPathProvidedException { - new JarModuleLoader(null); + @Test(expected = NoModuleToInstrumentException.class) + public void testConstructorFailWithNullConfig() { + new JarModuleLoader().loadPackages(null); } - @Test - public void testConstructorWorksAndHasDefaultStrategy() { - Assert.assertTrue(loader.getStrategy().getClass().equals(JarModuleExportStrategy.class)); + @Test(expected = NoModuleToInstrumentException.class) + public void testConstructorFailWithNullPathList() { + new JarModuleLoader().loadPackages(PreprocessConfig.builder().build()); } @Test - public void testConstructorWorksAndNoDuplicatePaths() { - Assert.assertTrue(loader.getPaths().size() == PATHS.size() - 1); - - for (String path : PATHS) { - Assert.assertTrue(loader.getPaths().contains(path)); - } + public void testConstructorWorksAndHasDefaultStrategy() { + Assert.assertTrue(loader.getStrategy().getClass().equals(JarModuleExportStrategy.class)); } @Test public void testConstructorWorksWithNonDefaultStrategy() { ModuleExportStrategy mockStrategy = Mockito.mock(ModuleExportStrategy.class); - loader = new JarModuleLoader(mockStrategy, PATHS); + loader = new JarModuleLoader(mockStrategy); Assert.assertNotEquals(JarModuleExportStrategy.class, loader.getStrategy().getClass()); } @@ -127,48 +124,43 @@ public void testProcessFileWorksAndInvokesInjectFileToSystemClassPath() { Mockito.verify(packageLoader).injectFileToSystemClassPath(mockFile); } - @Test(expected = NoModuleToInstrumentException.class) - public void testLoadPackagesFailOnEmptyPackageInfoList() { - loader.loadPackages(); - } - @Test public void testLoadPackagesWorksWithOnePackageInfo() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(Arrays.asList(PATHS.get(0)))); + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); - Mockito.doCallRealMethod().when(packageLoader).loadPackages(); + Mockito.doCallRealMethod().when(packageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(0))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); Mockito.doReturn(MockEntities.makeMockPackageInfo()).when(packageLoader).loadPackage(MOCK_FILES.get(0)); - packageLoader.loadPackages(); + packageLoader.loadPackages(config); Mockito.verify(packageLoader).loadPackage(Mockito.any()); } @Test(expected = NoModuleToInstrumentException.class) public void testLoadPackagesFailsWithNoPackageInfoCreated() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(Arrays.asList(PATHS.get(0)))); + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); - Mockito.doCallRealMethod().when(packageLoader).loadPackages(); + Mockito.doCallRealMethod().when(packageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(0))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); - packageLoader.loadPackages(); + packageLoader.loadPackages(config); Mockito.verify(packageLoader).loadPackage(Mockito.any()); } @Test public void testLoadPackagesWorksAndCalledThreeTimesWithThreePaths() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(PATHS)); + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); - Mockito.doCallRealMethod().when(packageLoader).loadPackages(); + Mockito.doCallRealMethod().when(packageLoader).loadPackages(config); Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(1))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(2))).thenReturn(Arrays.asList(MOCK_FILES.get(1))); Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(3))).thenReturn(Arrays.asList(MOCK_FILES.get(2))); try { - packageLoader.loadPackages(); + packageLoader.loadPackages(config); } catch (NoModuleToInstrumentException e) { // swallow } @@ -178,7 +170,7 @@ public void testLoadPackagesWorksAndCalledThreeTimesWithThreePaths() { @Test public void testLoadPackagesWorksAndReturnsValidPackageInfoObjectAndInvokesProcessFile() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(new JarModuleExportStrategy(), PATHS)); + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(new JarModuleExportStrategy())); List classes = MOCK_JAR_ENTRIES .stream() diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/DiscoAgentTemplate.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/DiscoAgentTemplate.java index 8290c3a..148026d 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/DiscoAgentTemplate.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/DiscoAgentTemplate.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Set; +import java.util.function.Supplier; /** * All agent products have different needs and use-cases, but initialization is super similar between them. @@ -40,8 +41,9 @@ */ public class DiscoAgentTemplate { private static final Logger log = LogManager.getLogger(DiscoAgentTemplate.class); + private static Supplier agentConfigFactory = null; - private final AgentConfig config; + protected final AgentConfig config; private InterceptionInstaller interceptionInstaller = InterceptionInstaller.getInstance(); private ElementMatcher.Junction customIgnoreMatcher = ElementMatchers.none(); private boolean allowPlugins = true; @@ -53,7 +55,12 @@ public class DiscoAgentTemplate { * @param agentArgs any arguments passed as part of the -javaagent argument string */ public DiscoAgentTemplate(String agentArgs) { - this.config = new AgentConfigParser().parseCommandLine(agentArgs); + if (agentConfigFactory == null) { + this.config = new AgentConfigParser().parseCommandLine(agentArgs); + } else { + this.config = agentConfigFactory.get(); + } + if (config.getLoggerFactoryClass() != null) { try { LoggerFactory loggerFactory = LoggerFactory.class.cast(Class.forName(config.getLoggerFactoryClass(), true, ClassLoader.getSystemClassLoader()).newInstance()); @@ -159,4 +166,22 @@ InterceptionInstaller setInterceptionInstaller(InterceptionInstaller interceptio this.interceptionInstaller = interceptionInstaller; return old; } + + /** + * Override the default AgentConfig behavior, which is to read from the command line, if there is an alternative source of configuration. + * Must be called before constructing DiscoAgentTemplate. + * @param factory the new supplier of an AgentConfig to use. + */ + public static void setAgentConfigFactory(Supplier factory) { + agentConfigFactory = factory; + } + + /** + * Retrieve the current AgentConfigFactory + * + * @return the currently configured AgentConfigFactory, null if unset. + */ + public static Supplier getAgentConfigFactory() { + return agentConfigFactory; + } } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java index 5214d88..23a5597 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/config/AgentConfig.java @@ -58,6 +58,9 @@ public AgentBuilder apply(AgentBuilder agentBuilder, Installable installable) { * This transformer will be invoked in InterceptionInstaller to apply transformations on all Installables of * a given agent. Passing a null value will reset the AgentBuilderTransformer to the default value: {@link NoOpAgentBuilderTransformer} * + * If a brand new instance is created by the transformer and returned while ignoring the passed in AgentBuilder, the default ignore rule and debugger listener + * set by disco core will be lost. See {@link software.amazon.disco.agent.interception.InterceptionInstaller} for more detail on how they are set. + * * @param agentBuilderTransformer the AgentBuilder Transformer to be applied to an AgentBuilder */ public void setAgentBuilderTransformer(BiFunction agentBuilderTransformer) { diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java index decb144..18bc979 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/DiscoAgentTemplateTests.java @@ -17,6 +17,7 @@ import org.mockito.Spy; import software.amazon.disco.agent.concurrent.TransactionContext; +import software.amazon.disco.agent.config.AgentConfig; import software.amazon.disco.agent.interception.Installable; import software.amazon.disco.agent.interception.InterceptionInstaller; import software.amazon.disco.agent.logging.LogManager; @@ -38,6 +39,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.function.Supplier; import static org.junit.Assert.assertEquals; @@ -61,6 +63,7 @@ public void before() { @After public void after() { TransactionContext.clear(); + DiscoAgentTemplate.setAgentConfigFactory(null); } @@ -95,6 +98,28 @@ public void testArgumentHandlerCalled() { Mockito.verify(mock).handleArguments(Mockito.eq(args)); } + @Test + public void testSetAgentConfigFactory(){ + Assert.assertNull(DiscoAgentTemplate.getAgentConfigFactory()); + DiscoAgentTemplate.setAgentConfigFactory(()->null); + Assert.assertNotNull(DiscoAgentTemplate.getAgentConfigFactory()); + } + + @Test + public void testConstructorInvokesAgentConfigFactory(){ + List args = Mockito.mock(List.class); + Supplier factory = Mockito.mock(Supplier.class); + + Mockito.when(factory.get()).thenReturn(new AgentConfig(args)); + DiscoAgentTemplate.setAgentConfigFactory(factory); + + DiscoAgentTemplate template = new DiscoAgentTemplate(null); + + Mockito.verify(factory).get(); + Assert.assertNotNull(DiscoAgentTemplate.getAgentConfigFactory()); + Assert.assertSame(args, template.config.getArgs()); + } + private DiscoAgentTemplate createDiscoAgentTemplate(String... args) { List argsList = new LinkedList<>(Arrays.asList(args)); argsList.add("domain=DOMAIN"); From a11cd1725b7479e684b5713e003f20049a98be54 Mon Sep 17 00:00:00 2001 From: William Armiros Date: Tue, 21 Jul 2020 15:38:04 -0700 Subject: [PATCH 33/45] Added SQL interception library --- disco-java-agent-sql/README.md | 20 +- disco-java-agent-sql/build.gradle.kts | 1 + .../build.gradle.kts | 2 +- .../amazon/disco/agent/integtest/sql/.gitkeep | 1 - .../sql/JdbcExecuteInterceptorTest.java | 4 + .../agent/sql/JdbcExecuteInterceptor.java | 210 +++++++ .../amazon/disco/agent/sql/SQLSupport.java | 5 - .../amazon/disco/agent/sql/SqlSupport.java | 21 + .../software/amazon/disco/agent/sql/.gitkeep | 1 - .../agent/sql/JdbcExecuteInterceptorTest.java | 274 ++++++++ .../disco/agent/sql/SqlSupportTest.java | 19 + .../sql/source/MyCallableStatementImpl.java | 592 ++++++++++++++++++ .../sql/source/MyPreparedStatementImpl.java | 299 +++++++++ .../agent/sql/source/MyStatementImpl.java | 234 +++++++ 14 files changed, 1674 insertions(+), 9 deletions(-) delete mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/.gitkeep create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/JdbcExecuteInterceptorTest.java create mode 100644 disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptor.java delete mode 100644 disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SQLSupport.java create mode 100644 disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SqlSupport.java delete mode 100644 disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/.gitkeep create mode 100644 disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptorTest.java create mode 100644 disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/SqlSupportTest.java create mode 100644 disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyCallableStatementImpl.java create mode 100644 disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyPreparedStatementImpl.java create mode 100644 disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyStatementImpl.java diff --git a/disco-java-agent-sql/README.md b/disco-java-agent-sql/README.md index dc65562..70669c7 100644 --- a/disco-java-agent-sql/README.md +++ b/disco-java-agent-sql/README.md @@ -6,9 +6,27 @@ Event producer for popular frameworks used in service oriented software, this su 1. In this folder, the Installables to intercept SQL interactions, and issue appropriate Event Bus Events. 1. In the disco-java-agent-sql-plugin subfolder, a proper Disco plugin, bundled as a plugin JAR file with Manifest. +## Feature Status + +All methods in the table below are intercepted and published to the Event Bus as a pair of +`ServiceDownstreamRequestEvent`, published immediately before the DB Driver begins their implementation of the method, +and `ServiceDownstreamResponseEvent`, published immediately after the method ends either successfully or due to an +exception. + +The Statement object will be captured as the request, the Database name as the service, and the query string as the +operation on a best-effort basis. If you can't retrieve query strings from PreparedStatement objects, disco cannot +include them in events. + +| | execute | executeQuery | executeUpdate | executeLargeUpdate | executeBatch | +|-------------------|--------------------|--------------------|--------------------|--------------------|--------------------------| +| Statement | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_multiplication_x: | +| PreparedStatement | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_multiplication_x: | +| CallableStatement | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_multiplication_x: | + ## Package description -TODO +`SqlSupport` is a Disco Package that can be installed by standalone Agents to gain interception and +event publication for the features described above. ## Why the separation into two projects? diff --git a/disco-java-agent-sql/build.gradle.kts b/disco-java-agent-sql/build.gradle.kts index 946f4af..caa2c5f 100644 --- a/disco-java-agent-sql/build.gradle.kts +++ b/disco-java-agent-sql/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(project(":disco-java-agent:disco-java-agent-core")) testImplementation("org.mockito", "mockito-core", "1.+") + testImplementation("mysql", "mysql-connector-java", "8.+") } configure { diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts b/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts index 84265cb..9e47df3 100644 --- a/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { tasks.shadowJar { manifest { attributes(mapOf( - "Disco-Installable-Classes" to "software.amazon.disco.agent.sql.SQLSupport" + "Disco-Installable-Classes" to "software.amazon.disco.agent.sql.SqlSupport" )) } } diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/.gitkeep b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/.gitkeep deleted file mode 100644 index 8a05bb5..0000000 --- a/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -Integ tests TODO \ No newline at end of file diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/JdbcExecuteInterceptorTest.java b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/JdbcExecuteInterceptorTest.java new file mode 100644 index 0000000..e0f2638 --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/JdbcExecuteInterceptorTest.java @@ -0,0 +1,4 @@ +package software.amazon.disco.agent.integtest.sql; + +public class JdbcExecuteInterceptorTest { +} diff --git a/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptor.java b/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptor.java new file mode 100644 index 0000000..2d83b90 --- /dev/null +++ b/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptor.java @@ -0,0 +1,210 @@ +package software.amazon.disco.agent.sql; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import software.amazon.disco.agent.event.EventBus; +import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; +import software.amazon.disco.agent.event.ServiceRequestEvent; +import software.amazon.disco.agent.event.ServiceResponseEvent; +import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.agent.logging.LogManager; +import software.amazon.disco.agent.logging.Logger; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +/** + * This class represents the Disco interception of SQL queries performed using the JDBC. Some relevant information + * to know about the JDBC is that there are 3 Statement interfaces, which are implemented by various SQL databases + * such as MySQL. Statement classes are used to define queries then execute them. The inheritance of the Statement + * interfaces is: + * + * CallableStatement extends PreparedStatement extends Statement + * + * Callable and Prepared statements are typically used for queries whose query strings are defined in advanced and + * reused several times, whereas the base Statement class is more often used for one-off queries. Each statement + * interface defines three methods to execute queries, which are explained more below. + */ +public class JdbcExecuteInterceptor implements Installable { + private static final Logger log = LogManager.getLogger(JdbcExecuteInterceptor.class); + static final String SQL_ORIGIN = "SQL"; + + /** + * This method is inlined at the beginning of all execute methods matched by {@link #buildMethodMatcher}. It + * extracts some information from the intercepted execute method and calling object, then publishes a SQL query + * request event as a {@link ServiceDownstreamRequestEvent}. The Query "service" is modeled as: + * + * Origin = "SQL" + * Service = Target database name + * Operation = SQL query string + * Request = JDBC Statement object constructed by user, intercepted by ByteBuddy + * + * Must be public for use in Advice methods https://github.com/raphw/byte-buddy/issues/761 + * + * @param args parameters passed to execute method, potentially including the query string + * @param origin Identifier of the intercepted method, for debugging/logging + * @param stmt concrete statement class being used to make the query + * @return a ServiceDownstreamRequestEvent with fields populated on a best effort basis + */ + @SuppressWarnings("unused") + @Advice.OnMethodEnter + public static ServiceRequestEvent enter(@Advice.AllArguments final Object[] args, + @Advice.Origin final String origin, + @Advice.This final Statement stmt) { + if (LogManager.isDebugEnabled()) { + log.debug("DiSCo(Sql) interception of " + origin); + } + + String query = null; + String db = null; + try { + query = parseQueryFromStatement(stmt, args); + } catch (Exception e) { + log.warn("Disco(Sql) failed to retrieve query string for SQL Downstream Service event", e); + } + + try { + db = stmt.getConnection().getCatalog(); + } catch (Exception e) { + log.warn("Disco(Sql) failed to retrieve Database name for SQL Downstream Service event", e); + } + + // TODO: Consider replacing Statement Request object with a serializable object containing only relevant metadata + ServiceRequestEvent requestEvent = new ServiceDownstreamRequestEvent(SQL_ORIGIN, db, query) + .withRequest(stmt); + EventBus.publish(requestEvent); + return requestEvent; + } + + /** + * This method is inlined with an execute method at the moment it would return or throw a {@link Throwable}. + * It extracts the response and throwable if any and publishes a {@link ServiceDownstreamResponseEvent}. + * See {@link #enter} for Event model. + * + * Must be public for use in Advice methods https://github.com/raphw/byte-buddy/issues/761 + * + * @param requestEvent the returned value of {@link #enter} method, passed in using the Enter annotation + * @param response the response of the JDBC execute method or null if an exception was thrown, passed in using the + * Return annotation + * @param thrown the Throwable thrown by the query, or null if query was successful. Passed in using the Thrown + * annotation. Typically a {@link SQLException}. + */ + @SuppressWarnings("unused") + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void exit(@Advice.Enter final ServiceRequestEvent requestEvent, + @Advice.Return final Object response, + @Advice.Thrown final Throwable thrown) { + + ServiceResponseEvent responseEvent = new ServiceDownstreamResponseEvent( + SQL_ORIGIN, + requestEvent.getOperation(), + requestEvent.getService(), + requestEvent) + .withResponse(response) + .withThrown(thrown); + + EventBus.publish(responseEvent); + } + + /** + * Installs the Disco SQL interception library into a Java program. Intended to be invoked + * during an agent's premain. + * + * @param agentBuilder - an AgentBuilder to append instructions to + * @return - the {@code AgentBuilder} object for chaining + */ + @Override + public AgentBuilder install(final AgentBuilder agentBuilder) { + return agentBuilder + .type(buildClassMatcher()) + .transform(new AgentBuilder.Transformer.ForAdvice() + .advice(buildMethodMatcher(), JdbcExecuteInterceptor.class.getName())); + } + + /** + * This helper method attempts to get the query string in two ways before giving up and returning null. The first is + * just retrieving it from the arguments passed to the execute method being intercepted. If it is not present there, + * then we must be dealing with a Prepared or Callable statement that had its query string pre-loaded rather than + * passed as an argument. In this case, there is no way provided by the JDBC to extract the query. However many DB + * Drivers implement the {@code toString} method on their PreparedStatement class to return the pre-loaded SQL query + * string. So the second way is to check if the {@code toString} method is overridden, and if so we use it. + * + * See: https://stackoverflow.com/questions/2382532/how-can-i-get-the-sql-of-a-preparedstatement + * + * Must be public for use in Advice methods https://github.com/raphw/byte-buddy/issues/761 + * + * @param stmt the JDBC Statement object being used to make the query + * @param params the parameters passed for the execution of the SQL query. If they exist, they must contain the + * SQL query string. + * @return the query string used in this SQL query if readable, or {@code null} otherwise + */ + public static String parseQueryFromStatement(Statement stmt, Object[] params) throws NoSuchMethodException { + String query = null; + if (params != null && params.length > 0 && params[0] instanceof String) { + query = (String) params[0]; + } else if (stmt instanceof PreparedStatement && Statement.class.isAssignableFrom(stmt.getClass().getMethod("toString").getDeclaringClass())) { + query = stmt.toString(); + } + + return query; + } + + /** + * Builds an element matcher that will match any implementation of the Statement classes: + * Statement, PreparedStatement, and CallableStatement. Checking for the Statement class super type is sufficient + * because the other two are just extensions of Statement. + * Exposed for testing. + * + * @return - An ElementMatcher suitable to pass to the type() method of an AgentBuilder + */ + static ElementMatcher buildClassMatcher() { + return hasSuperType(named("java.sql.Statement")) + .and(not(isInterface())); + } + + /** + * Builds an ElementMatcher for the 4 methods used by statements to carry out a SQL query: + * execute, executeQuery, and executeUpdate, and executeLargeUpdate. All 4 methods can be called with a + * String parameter containing the SQL query string in all Statement classes. However, in the PreparedStatement and + * CallableStatement classes, the first 3 are implemented without any parameters, because the query string is + * passed when the Statement is constructed instead. So we match against normal statements that take a string + * parameter, or prepared/callable statements that do not. + * Exposed for testing. + * + * @return - An ElementMatcher that can match one of the methods to perform a SQL query + */ + static ElementMatcher buildMethodMatcher() { + // These are used for Prepared and Callable statements where execute methods do not have to take args + ElementMatcher.Junction preparedStatementMatcher = hasSuperType(named("java.sql.PreparedStatement")).and(not(isInterface())); + ElementMatcher.Junction takesNoArgsMatcher = isDeclaredBy(preparedStatementMatcher).and(takesArguments(0)); + + // This is for regular Statements which always provide a SQL string as the first arg to execute methods + ElementMatcher.Junction takesSqlArgMatcher = isDeclaredBy(buildClassMatcher()).and(takesArgument(0, String.class)); + + ElementMatcher.Junction executeMatcher = named("execute").and(returns(boolean.class)); + ElementMatcher.Junction executeUpdateMatcher = named("executeUpdate").and(returns(int.class)); + ElementMatcher.Junction executeLargeUpdateMatcher = named("executeLargeUpdate").and(returns(long.class)); + ElementMatcher.Junction executeQueryMatcher = named("executeQuery").and(returns(ResultSet.class)); + + ElementMatcher.Junction methodNameMatches = executeMatcher.or(executeUpdateMatcher).or(executeQueryMatcher).or(executeLargeUpdateMatcher); + ElementMatcher.Junction argumentTypesCorrect = takesNoArgsMatcher.or(takesSqlArgMatcher); + + return methodNameMatches.and(argumentTypesCorrect).and(not(isAbstract())); + } +} diff --git a/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SQLSupport.java b/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SQLSupport.java deleted file mode 100644 index c897f1f..0000000 --- a/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SQLSupport.java +++ /dev/null @@ -1,5 +0,0 @@ -package software.amazon.disco.agent.sql; - -public class SQLSupport { - // TODO - implement SQL Interceptor -} diff --git a/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SqlSupport.java b/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SqlSupport.java new file mode 100644 index 0000000..bd410ab --- /dev/null +++ b/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/SqlSupport.java @@ -0,0 +1,21 @@ +package software.amazon.disco.agent.sql; + +import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.agent.interception.Package; + +import java.util.Arrays; +import java.util.Collection; + +/** + * Package definition for the disco-java-agent-sql package. + */ +public class SqlSupport implements Package { + + /** + * {@inheritDoc} + */ + @Override + public Collection get() { + return Arrays.asList(new JdbcExecuteInterceptor()); + } +} diff --git a/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/.gitkeep b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/.gitkeep deleted file mode 100644 index 216c279..0000000 --- a/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -Unit tests TODO \ No newline at end of file diff --git a/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptorTest.java b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptorTest.java new file mode 100644 index 0000000..516ce20 --- /dev/null +++ b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptorTest.java @@ -0,0 +1,274 @@ +package software.amazon.disco.agent.sql; + +import com.mysql.cj.jdbc.CallableStatement; +import com.mysql.cj.jdbc.ClientPreparedStatement; +import com.mysql.cj.jdbc.ConnectionImpl; +import com.mysql.cj.jdbc.StatementImpl; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.disco.agent.event.Event; +import software.amazon.disco.agent.event.EventBus; +import software.amazon.disco.agent.event.Listener; +import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.ServiceRequestEvent; +import software.amazon.disco.agent.event.ServiceResponseEvent; +import software.amazon.disco.agent.sql.source.MyCallableStatementImpl; +import software.amazon.disco.agent.sql.source.MyPreparedStatementImpl; +import software.amazon.disco.agent.sql.source.MyStatementImpl; + +import java.lang.reflect.Method; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class JdbcExecuteInterceptorTest { + private static final String DB_NAME = "My db"; + private static final String QUERY = "some sql"; + + private Statement myStatement; + private PreparedStatement myPreparedStatement; + private java.sql.CallableStatement myCallableStatement; + private JdbcExecuteInterceptor interceptor; + private ServiceRequestEvent requestEvent; + private MockEventBusListener mockListener; + + @Mock + ConnectionImpl mockConnection; + + @Mock + StatementImpl mockStatement; + + @Mock + ClientPreparedStatement mockPreparedStatement; + + @Mock + CallableStatement mockCallableStatement; + + @Before + public void setup() throws SQLException { + interceptor = new JdbcExecuteInterceptor(); + myStatement = new MyStatementImpl(); + myPreparedStatement = new MyPreparedStatementImpl(); + myCallableStatement = new MyCallableStatementImpl(); + requestEvent = new ServiceDownstreamRequestEvent(JdbcExecuteInterceptor.SQL_ORIGIN, DB_NAME, QUERY) + .withRequest(mockStatement); + mockListener = new MockEventBusListener(); + EventBus.addListener(mockListener); + + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.getConnection()).thenReturn(mockConnection); + when(mockConnection.getCatalog()).thenReturn(DB_NAME); + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockConnection.prepareCall(anyString())).thenReturn(mockCallableStatement); + } + + @Test + public void testInstallation() { + AgentBuilder agentBuilder = mock(AgentBuilder.class); + AgentBuilder.Identified.Extendable extendable = mock(AgentBuilder.Identified.Extendable.class); + AgentBuilder.Identified.Narrowable narrowable = mock(AgentBuilder.Identified.Narrowable.class); + when(agentBuilder.type(any(ElementMatcher.class))).thenReturn(narrowable); + when(narrowable.transform(any(AgentBuilder.Transformer.class))).thenReturn(extendable); + AgentBuilder result = interceptor.install(agentBuilder); + assertSame(extendable, result); + } + + @Test + public void testClassMatcherSucceedsOnRealStatements() throws SQLException { + assertTrue(classMatches(mockConnection.createStatement().getClass())); + assertTrue(classMatches(mockConnection.prepareCall("SOME SQL").getClass())); + assertTrue(classMatches(mockConnection.prepareStatement("MORE SQL").getClass())); + } + + @Test + public void testClassMatcherSucceedsOnConcreteStatements() { + assertTrue(classMatches(myStatement.getClass())); + assertTrue(classMatches(myPreparedStatement.getClass())); + assertTrue(classMatches(myCallableStatement.getClass())); + } + + @Test + public void testInvalidClassesDontMatch() { + assertFalse(classMatches(String.class)); + assertFalse(classMatches(Statement.class)); // Interface + } + + @Test + public void testRealExecuteWithStringMatches() throws NoSuchMethodException { + assertEquals(4, methodMatchedCount("execute", StatementImpl.class)); + assertEquals(4, methodMatchedCount("executeUpdate", StatementImpl.class)); + assertEquals(4, methodMatchedCount("executeLargeUpdate", StatementImpl.class)); + assertEquals(1, methodMatchedCount("executeQuery", StatementImpl.class)); + } + + @Test + public void testRealExecuteWithoutStringMatches() throws NoSuchMethodException { + assertEquals(1, methodMatchedCount("execute", ClientPreparedStatement.class)); + assertEquals(1, methodMatchedCount("executeUpdate", ClientPreparedStatement.class)); + assertEquals(1, methodMatchedCount("executeQuery", ClientPreparedStatement.class)); + } + + @Test + public void testNonExecuteMethodDoesNotMatch() throws NoSuchMethodException { + assertEquals(0, methodMatchedCount("addBatch", StatementImpl.class)); + } + + @Test + public void testConcreteExecuteWithStringMatches() throws NoSuchMethodException { + assertEquals(4, methodMatchedCount("execute", MyStatementImpl.class)); + assertEquals(4, methodMatchedCount("executeUpdate", MyStatementImpl.class)); + assertEquals(1, methodMatchedCount("executeQuery", MyStatementImpl.class)); + } + + @Test + public void testConcreteExecuteWithoutStringMatches() throws NoSuchMethodException { + assertEquals(1, methodMatchedCount("execute", MyPreparedStatementImpl.class)); + assertEquals(1, methodMatchedCount("executeUpdate", MyPreparedStatementImpl.class)); + assertEquals(1, methodMatchedCount("executeQuery", MyPreparedStatementImpl.class)); + } + + @Test + public void testAbstractExecuteDoesNotMatch() throws NoSuchMethodException { + assertEquals(0, methodMatchedCount("execute", MyStatementImpl.MyAbstractStatement.class)); + } + + @Test + public void testQueryStringExtractedFromParams() throws NoSuchMethodException { + Object[] params = {QUERY}; + String parsed = JdbcExecuteInterceptor.parseQueryFromStatement(myStatement, params); + + assertEquals(QUERY, parsed); + } + + @Test + public void testQueryStringExtractedWithToString() throws NoSuchMethodException { + Object[] params = {}; + String parsed = JdbcExecuteInterceptor.parseQueryFromStatement(myCallableStatement, params); + + // MyCallableStatement has toString() overloaded + assertEquals(myCallableStatement.toString(), parsed); + } + + @Test + public void testQueryStringNotExtracted() throws NoSuchMethodException { + Object[] params = {}; + String parsed = JdbcExecuteInterceptor.parseQueryFromStatement(myPreparedStatement, params); + + // MyPreparedStatement does not have toString() overloaded + assertNull(parsed); + } + + @Test + public void testRequestEventCreation() { + Object[] params = {QUERY}; + ServiceRequestEvent event = JdbcExecuteInterceptor.enter(params, null, mockStatement); + + assertEquals(JdbcExecuteInterceptor.SQL_ORIGIN, event.getOrigin()); + assertEquals(DB_NAME, event.getService()); + assertEquals(QUERY, event.getOperation()); + assertEquals(mockStatement, event.getRequest()); + } + + @Test + public void testRequestEventPublish() { + Object[] params = {QUERY}; + ServiceRequestEvent event = JdbcExecuteInterceptor.enter(params, null, mockStatement); + + assertEquals(event, mockListener.getReceivedEvents().get(0)); + } + + @Test + public void testResponseEventPublish() { + JdbcExecuteInterceptor.exit(requestEvent, 1, null); + + ServiceResponseEvent received = (ServiceResponseEvent) mockListener.getReceivedEvents().get(0); + + assertEquals(requestEvent, received.getRequest()); + assertEquals(1, received.getResponse()); + assertNull(received.getThrown()); + } + + @Test + public void testResponseEventWithThrownPublish() { + Exception ex = new SQLException(); + JdbcExecuteInterceptor.exit(requestEvent, null, ex); + + ServiceResponseEvent received = (ServiceResponseEvent) mockListener.getReceivedEvents().get(0); + + assertEquals(requestEvent, received.getRequest()); + assertEquals(ex, received.getThrown()); + assertNull(received.getResponse()); + } + + private boolean classMatches(Class clazz) { + return JdbcExecuteInterceptor.buildClassMatcher().matches(new TypeDescription.ForLoadedType(clazz)); + } + + /** + * Helper method to test the method matcher against an input class + * + * @param methodName name of method + * @param paramType class we are verifying contains the method + * @return Matched methods count + * @throws NoSuchMethodException + */ + private int methodMatchedCount(String methodName, Class paramType) throws NoSuchMethodException { + List methods = new ArrayList<>(); + for (Method m : paramType.getDeclaredMethods()) { + if (m.getName().equals(methodName)) { + methods.add(m); + } + } + + if (methods.size() == 0) throw new NoSuchMethodException(); + + int matchedCount = 0; + for (Method m : methods) { + MethodDescription.ForLoadedMethod forLoadedMethod = new MethodDescription.ForLoadedMethod(m); + if (JdbcExecuteInterceptor.buildMethodMatcher().matches(forLoadedMethod)) { + matchedCount++; + } + } + return matchedCount; + } + + private class MockEventBusListener implements Listener { + + + public List getReceivedEvents() { + return receivedEvents; + } + + private List receivedEvents = new ArrayList<>(); + + @Override + public int getPriority() { + return 0; + } + + @Override + public void listen(Event event) { + receivedEvents.add(event); + } + } +} diff --git a/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/SqlSupportTest.java b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/SqlSupportTest.java new file mode 100644 index 0000000..1de736f --- /dev/null +++ b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/SqlSupportTest.java @@ -0,0 +1,19 @@ +package software.amazon.disco.agent.sql; + +import org.junit.Assert; +import org.junit.Test; +import software.amazon.disco.agent.interception.Installable; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class SqlSupportTest { + @Test + public void testSqlSupport() { + Collection pkg = new SqlSupport().get(); + Set installables = new HashSet<>(); + installables.addAll(pkg); + Assert.assertEquals(1, installables.size()); + } +} diff --git a/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyCallableStatementImpl.java b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyCallableStatementImpl.java new file mode 100644 index 0000000..a9da265 --- /dev/null +++ b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyCallableStatementImpl.java @@ -0,0 +1,592 @@ +package software.amazon.disco.agent.sql.source; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +public class MyCallableStatementImpl extends MyPreparedStatementImpl implements CallableStatement { + @Override + public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException { + + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException { + + } + + @Override + public boolean wasNull() throws SQLException { + return false; + } + + @Override + public String getString(int parameterIndex) throws SQLException { + return null; + } + + @Override + public boolean getBoolean(int parameterIndex) throws SQLException { + return false; + } + + @Override + public byte getByte(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public short getShort(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public int getInt(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public long getLong(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public float getFloat(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public double getDouble(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException { + return null; + } + + @Override + public byte[] getBytes(int parameterIndex) throws SQLException { + return new byte[0]; + } + + @Override + public Date getDate(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Time getTime(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Timestamp getTimestamp(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Object getObject(int parameterIndex) throws SQLException { + return null; + } + + @Override + public BigDecimal getBigDecimal(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Object getObject(int parameterIndex, Map> map) throws SQLException { + return null; + } + + @Override + public Ref getRef(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Blob getBlob(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Clob getClob(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Array getArray(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Date getDate(int parameterIndex, Calendar cal) throws SQLException { + return null; + } + + @Override + public Time getTime(int parameterIndex, Calendar cal) throws SQLException { + return null; + } + + @Override + public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException { + return null; + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, String typeName) throws SQLException { + + } + + @Override + public void registerOutParameter(String parameterName, int sqlType) throws SQLException { + + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, int scale) throws SQLException { + + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, String typeName) throws SQLException { + + } + + @Override + public URL getURL(int parameterIndex) throws SQLException { + return null; + } + + @Override + public void setURL(String parameterName, URL val) throws SQLException { + + } + + @Override + public void setNull(String parameterName, int sqlType) throws SQLException { + + } + + @Override + public void setBoolean(String parameterName, boolean x) throws SQLException { + + } + + @Override + public void setByte(String parameterName, byte x) throws SQLException { + + } + + @Override + public void setShort(String parameterName, short x) throws SQLException { + + } + + @Override + public void setInt(String parameterName, int x) throws SQLException { + + } + + @Override + public void setLong(String parameterName, long x) throws SQLException { + + } + + @Override + public void setFloat(String parameterName, float x) throws SQLException { + + } + + @Override + public void setDouble(String parameterName, double x) throws SQLException { + + } + + @Override + public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException { + + } + + @Override + public void setString(String parameterName, String x) throws SQLException { + + } + + @Override + public void setBytes(String parameterName, byte[] x) throws SQLException { + + } + + @Override + public void setDate(String parameterName, Date x) throws SQLException { + + } + + @Override + public void setTime(String parameterName, Time x) throws SQLException { + + } + + @Override + public void setTimestamp(String parameterName, Timestamp x) throws SQLException { + + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException { + + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException { + + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException { + + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException { + + } + + @Override + public void setObject(String parameterName, Object x) throws SQLException { + + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, int length) throws SQLException { + + } + + @Override + public void setDate(String parameterName, Date x, Calendar cal) throws SQLException { + + } + + @Override + public void setTime(String parameterName, Time x, Calendar cal) throws SQLException { + + } + + @Override + public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException { + + } + + @Override + public void setNull(String parameterName, int sqlType, String typeName) throws SQLException { + + } + + @Override + public String getString(String parameterName) throws SQLException { + return null; + } + + @Override + public boolean getBoolean(String parameterName) throws SQLException { + return false; + } + + @Override + public byte getByte(String parameterName) throws SQLException { + return 0; + } + + @Override + public short getShort(String parameterName) throws SQLException { + return 0; + } + + @Override + public int getInt(String parameterName) throws SQLException { + return 0; + } + + @Override + public long getLong(String parameterName) throws SQLException { + return 0; + } + + @Override + public float getFloat(String parameterName) throws SQLException { + return 0; + } + + @Override + public double getDouble(String parameterName) throws SQLException { + return 0; + } + + @Override + public byte[] getBytes(String parameterName) throws SQLException { + return new byte[0]; + } + + @Override + public Date getDate(String parameterName) throws SQLException { + return null; + } + + @Override + public Time getTime(String parameterName) throws SQLException { + return null; + } + + @Override + public Timestamp getTimestamp(String parameterName) throws SQLException { + return null; + } + + @Override + public Object getObject(String parameterName) throws SQLException { + return null; + } + + @Override + public BigDecimal getBigDecimal(String parameterName) throws SQLException { + return null; + } + + @Override + public Object getObject(String parameterName, Map> map) throws SQLException { + return null; + } + + @Override + public Ref getRef(String parameterName) throws SQLException { + return null; + } + + @Override + public Blob getBlob(String parameterName) throws SQLException { + return null; + } + + @Override + public Clob getClob(String parameterName) throws SQLException { + return null; + } + + @Override + public Array getArray(String parameterName) throws SQLException { + return null; + } + + @Override + public Date getDate(String parameterName, Calendar cal) throws SQLException { + return null; + } + + @Override + public Time getTime(String parameterName, Calendar cal) throws SQLException { + return null; + } + + @Override + public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException { + return null; + } + + @Override + public URL getURL(String parameterName) throws SQLException { + return null; + } + + @Override + public RowId getRowId(int parameterIndex) throws SQLException { + return null; + } + + @Override + public RowId getRowId(String parameterName) throws SQLException { + return null; + } + + @Override + public void setRowId(String parameterName, RowId x) throws SQLException { + + } + + @Override + public void setNString(String parameterName, String value) throws SQLException { + + } + + @Override + public void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException { + + } + + @Override + public void setNClob(String parameterName, NClob value) throws SQLException { + + } + + @Override + public void setClob(String parameterName, Reader reader, long length) throws SQLException { + + } + + @Override + public void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException { + + } + + @Override + public void setNClob(String parameterName, Reader reader, long length) throws SQLException { + + } + + @Override + public NClob getNClob(int parameterIndex) throws SQLException { + return null; + } + + @Override + public NClob getNClob(String parameterName) throws SQLException { + return null; + } + + @Override + public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException { + + } + + @Override + public SQLXML getSQLXML(int parameterIndex) throws SQLException { + return null; + } + + @Override + public SQLXML getSQLXML(String parameterName) throws SQLException { + return null; + } + + @Override + public String getNString(int parameterIndex) throws SQLException { + return null; + } + + @Override + public String getNString(String parameterName) throws SQLException { + return null; + } + + @Override + public Reader getNCharacterStream(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Reader getNCharacterStream(String parameterName) throws SQLException { + return null; + } + + @Override + public Reader getCharacterStream(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Reader getCharacterStream(String parameterName) throws SQLException { + return null; + } + + @Override + public void setBlob(String parameterName, Blob x) throws SQLException { + + } + + @Override + public void setClob(String parameterName, Clob x) throws SQLException { + + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException { + + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException { + + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException { + + } + + @Override + public void setAsciiStream(String parameterName, InputStream x) throws SQLException { + + } + + @Override + public void setBinaryStream(String parameterName, InputStream x) throws SQLException { + + } + + @Override + public void setCharacterStream(String parameterName, Reader reader) throws SQLException { + + } + + @Override + public void setNCharacterStream(String parameterName, Reader value) throws SQLException { + + } + + @Override + public void setClob(String parameterName, Reader reader) throws SQLException { + + } + + @Override + public void setBlob(String parameterName, InputStream inputStream) throws SQLException { + + } + + @Override + public void setNClob(String parameterName, Reader reader) throws SQLException { + + } + + @Override + public T getObject(int parameterIndex, Class type) throws SQLException { + return null; + } + + @Override + public T getObject(String parameterName, Class type) throws SQLException { + return null; + } + + @Override + public String toString() { + return "a sql query"; + } +} diff --git a/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyPreparedStatementImpl.java b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyPreparedStatementImpl.java new file mode 100644 index 0000000..aaf28cb --- /dev/null +++ b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyPreparedStatementImpl.java @@ -0,0 +1,299 @@ +package software.amazon.disco.agent.sql.source; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; + +public class MyPreparedStatementImpl extends MyStatementImpl implements PreparedStatement { + @Override + public ResultSet executeQuery() throws SQLException { + return null; + } + + @Override + public int executeUpdate() throws SQLException { + return 0; + } + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + + } + + @Override + public void setString(int parameterIndex, String x) throws SQLException { + + } + + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + + } + + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + + } + + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + + } + + @Override + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + + } + + @Override + public void clearParameters() throws SQLException { + + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + + } + + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + + } + + @Override + public boolean execute() throws SQLException { + return false; + } + + @Override + public void addBatch() throws SQLException { + + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + + } + + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + + } + + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + + } + + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + + } + + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return null; + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + + } + + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + + } + + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + return null; + } + + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + + } + + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + + } + + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + + } + + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + + } + + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + + } + + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + + } + + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + + } +} diff --git a/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyStatementImpl.java b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyStatementImpl.java new file mode 100644 index 0000000..f705e17 --- /dev/null +++ b/disco-java-agent-sql/src/test/java/software/amazon/disco/agent/sql/source/MyStatementImpl.java @@ -0,0 +1,234 @@ +package software.amazon.disco.agent.sql.source; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; + +public class MyStatementImpl implements Statement { + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + return null; + } + + @Override + public int executeUpdate(String sql) throws SQLException { + return 0; + } + + @Override + public void close() throws SQLException { + + } + + @Override + public int getMaxFieldSize() throws SQLException { + return 0; + } + + @Override + public void setMaxFieldSize(int max) throws SQLException { + + } + + @Override + public int getMaxRows() throws SQLException { + return 0; + } + + @Override + public void setMaxRows(int max) throws SQLException { + + } + + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + + } + + @Override + public int getQueryTimeout() throws SQLException { + return 0; + } + + @Override + public void setQueryTimeout(int seconds) throws SQLException { + + } + + @Override + public void cancel() throws SQLException { + + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public void clearWarnings() throws SQLException { + + } + + @Override + public void setCursorName(String name) throws SQLException { + + } + + @Override + public boolean execute(String sql) throws SQLException { + return false; + } + + @Override + public ResultSet getResultSet() throws SQLException { + return null; + } + + @Override + public int getUpdateCount() throws SQLException { + return 0; + } + + @Override + public boolean getMoreResults() throws SQLException { + return false; + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + + } + + @Override + public int getFetchDirection() throws SQLException { + return 0; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + + } + + @Override + public int getFetchSize() throws SQLException { + return 0; + } + + @Override + public int getResultSetConcurrency() throws SQLException { + return 0; + } + + @Override + public int getResultSetType() throws SQLException { + return 0; + } + + @Override + public void addBatch(String sql) throws SQLException { + + } + + @Override + public void clearBatch() throws SQLException { + + } + + @Override + public int[] executeBatch() throws SQLException { + return new int[0]; + } + + @Override + public Connection getConnection() throws SQLException { + return null; + } + + @Override + public boolean getMoreResults(int current) throws SQLException { + return false; + } + + @Override + public ResultSet getGeneratedKeys() throws SQLException { + return null; + } + + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + return 0; + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + return 0; + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + return 0; + } + + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + return false; + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + return false; + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + return false; + } + + @Override + public int getResultSetHoldability() throws SQLException { + return 0; + } + + @Override + public boolean isClosed() throws SQLException { + return false; + } + + @Override + public void setPoolable(boolean poolable) throws SQLException { + + } + + @Override + public boolean isPoolable() throws SQLException { + return false; + } + + @Override + public void closeOnCompletion() throws SQLException { + + } + + @Override + public boolean isCloseOnCompletion() throws SQLException { + return false; + } + + @Override + public T unwrap(Class iface) throws SQLException { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + + public abstract class MyAbstractStatement extends MyStatementImpl { + public abstract int execute(); + } +} From d0534430b813f94ac0dae5709dea80101abfb796 Mon Sep 17 00:00:00 2001 From: William Armiros Date: Mon, 3 Aug 2020 13:45:10 -0700 Subject: [PATCH 34/45] added SQL interception integration tests --- .../sql/JdbcExecuteInterceptorTest.java | 161 +++++ .../sql/source/MyCallableStatementImpl.java | 593 ++++++++++++++++++ .../sql/source/MyPreparedStatementImpl.java | 304 +++++++++ .../integtest/sql/source/MyStatementImpl.java | 263 ++++++++ .../agent/sql/JdbcExecuteInterceptor.java | 7 +- 5 files changed, 1324 insertions(+), 4 deletions(-) create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyCallableStatementImpl.java create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyPreparedStatementImpl.java create mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyStatementImpl.java diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/JdbcExecuteInterceptorTest.java b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/JdbcExecuteInterceptorTest.java index e0f2638..4a625a7 100644 --- a/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/JdbcExecuteInterceptorTest.java +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/JdbcExecuteInterceptorTest.java @@ -1,4 +1,165 @@ package software.amazon.disco.agent.integtest.sql; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.disco.agent.event.Event; +import software.amazon.disco.agent.event.Listener; +import software.amazon.disco.agent.event.ServiceRequestEvent; +import software.amazon.disco.agent.event.ServiceResponseEvent; +import software.amazon.disco.agent.integtest.sql.source.MyCallableStatementImpl; +import software.amazon.disco.agent.integtest.sql.source.MyPreparedStatementImpl; +import software.amazon.disco.agent.integtest.sql.source.MyStatementImpl; +import software.amazon.disco.agent.reflect.event.EventBus; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) public class JdbcExecuteInterceptorTest { + private static final String QUERY = MyStatementImpl.QUERY; + private static final String DB = "MY_DB"; + + private TestListener listener; + private Statement statement; + private PreparedStatement preparedStatement; + private CallableStatement callableStatement; + + @Mock + Connection mockConnection; + + @Mock + ResultSet mockResultSet; + + @Before + public void setup() throws SQLException { + listener = new TestListener(); + EventBus.addListener(listener); + + statement = new MyStatementImpl(mockConnection, mockResultSet); + preparedStatement = new MyPreparedStatementImpl(mockConnection, mockResultSet); + callableStatement = new MyCallableStatementImpl(mockConnection, mockResultSet); + + when(mockConnection.getCatalog()).thenReturn(DB); + } + + @After + public void cleanup() throws Exception { + EventBus.removeAllListeners(); + } + + @Test + public void testExecuteQueryOnStatement() throws SQLException { + ResultSet rs = statement.executeQuery(QUERY); + + verifyRequestEvent(statement); + verifyResponseEvent(rs); + } + + @Test + public void testExecuteUpdateOnStatement() throws SQLException { + Integer res = statement.executeUpdate(QUERY); + + verifyRequestEvent(statement); + verifyResponseEvent(res); + } + + @Test + public void testExecuteLargeUpdateOnStatement() throws SQLException { + Long res = statement.executeLargeUpdate(QUERY); + + verifyRequestEvent(statement); + verifyResponseEvent(res); + } + + @Test + public void testExecuteOnStatement() throws SQLException { + Boolean res = statement.execute(QUERY); + + verifyRequestEvent(statement); + verifyResponseEvent(res); + } + + @Test + public void testExecuteWithAdditionalArgsOnStatement() throws SQLException { + Boolean res = statement.execute(QUERY, 0); // Tests JDBC 2.0 + + verifyRequestEvent(statement); + verifyResponseEvent(res); + } + + @Test + public void testExecuteQueryOnPreparedStatement() throws SQLException { + ResultSet rs = preparedStatement.executeQuery(); + + verifyRequestEvent(preparedStatement); + verifyResponseEvent(rs); + } + + @Test + public void testExecuteQueryOnCallableStatement() throws SQLException { + ResultSet rs = callableStatement.executeQuery(); + + verifyRequestEvent(callableStatement); + verifyResponseEvent(rs); + } + + @Test(expected = SQLException.class) + public void testExceptionCaughtAndThrown() throws SQLException { + try { + statement.executeUpdate(QUERY, new String[]{}); // This method implemented to throw exception + } finally { + verifyRequestEvent(statement); + Assert.assertEquals(1, listener.responseEvents.size()); + Assert.assertTrue(listener.responseEvents.get(0).getThrown() instanceof SQLException); + } + } + + private void verifyRequestEvent(Statement statement) { + Assert.assertEquals(1, listener.requestEvents.size()); + ServiceRequestEvent event = listener.requestEvents.get(0); + Assert.assertEquals(statement, event.getRequest()); + Assert.assertEquals(QUERY, event.getOperation()); + Assert.assertEquals(DB, event.getService()); + } + + private void verifyResponseEvent(Object response) { + Assert.assertEquals(1, listener.responseEvents.size()); + ServiceResponseEvent event = listener.responseEvents.get(0); + Assert.assertEquals(response, event.getResponse()); + Assert.assertNull(event.getThrown()); + } + + private static class TestListener implements Listener { + List requestEvents = new ArrayList<>(); + List responseEvents = new ArrayList<>(); + + @Override + public int getPriority() { + return 0; + } + + @Override + public void listen(Event e) { + if (e instanceof ServiceRequestEvent) { + requestEvents.add((ServiceRequestEvent) e); + } else if (e instanceof ServiceResponseEvent) { + responseEvents.add((ServiceResponseEvent) e); + } else { + Assert.fail("Unexpected event"); + } + } + } } diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyCallableStatementImpl.java b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyCallableStatementImpl.java new file mode 100644 index 0000000..390615c --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyCallableStatementImpl.java @@ -0,0 +1,593 @@ +package software.amazon.disco.agent.integtest.sql.source; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +public class MyCallableStatementImpl extends MyPreparedStatementImpl implements CallableStatement { + public MyCallableStatementImpl(Connection connection, ResultSet rs) { + super(connection, rs); + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException { + + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException { + + } + + @Override + public boolean wasNull() throws SQLException { + return false; + } + + @Override + public String getString(int parameterIndex) throws SQLException { + return null; + } + + @Override + public boolean getBoolean(int parameterIndex) throws SQLException { + return false; + } + + @Override + public byte getByte(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public short getShort(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public int getInt(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public long getLong(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public float getFloat(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public double getDouble(int parameterIndex) throws SQLException { + return 0; + } + + @Override + public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException { + return null; + } + + @Override + public byte[] getBytes(int parameterIndex) throws SQLException { + return new byte[0]; + } + + @Override + public Date getDate(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Time getTime(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Timestamp getTimestamp(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Object getObject(int parameterIndex) throws SQLException { + return null; + } + + @Override + public BigDecimal getBigDecimal(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Object getObject(int parameterIndex, Map> map) throws SQLException { + return null; + } + + @Override + public Ref getRef(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Blob getBlob(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Clob getClob(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Array getArray(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Date getDate(int parameterIndex, Calendar cal) throws SQLException { + return null; + } + + @Override + public Time getTime(int parameterIndex, Calendar cal) throws SQLException { + return null; + } + + @Override + public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException { + return null; + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, String typeName) throws SQLException { + + } + + @Override + public void registerOutParameter(String parameterName, int sqlType) throws SQLException { + + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, int scale) throws SQLException { + + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, String typeName) throws SQLException { + + } + + @Override + public URL getURL(int parameterIndex) throws SQLException { + return null; + } + + @Override + public void setURL(String parameterName, URL val) throws SQLException { + + } + + @Override + public void setNull(String parameterName, int sqlType) throws SQLException { + + } + + @Override + public void setBoolean(String parameterName, boolean x) throws SQLException { + + } + + @Override + public void setByte(String parameterName, byte x) throws SQLException { + + } + + @Override + public void setShort(String parameterName, short x) throws SQLException { + + } + + @Override + public void setInt(String parameterName, int x) throws SQLException { + + } + + @Override + public void setLong(String parameterName, long x) throws SQLException { + + } + + @Override + public void setFloat(String parameterName, float x) throws SQLException { + + } + + @Override + public void setDouble(String parameterName, double x) throws SQLException { + + } + + @Override + public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException { + + } + + @Override + public void setString(String parameterName, String x) throws SQLException { + + } + + @Override + public void setBytes(String parameterName, byte[] x) throws SQLException { + + } + + @Override + public void setDate(String parameterName, Date x) throws SQLException { + + } + + @Override + public void setTime(String parameterName, Time x) throws SQLException { + + } + + @Override + public void setTimestamp(String parameterName, Timestamp x) throws SQLException { + + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException { + + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException { + + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException { + + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException { + + } + + @Override + public void setObject(String parameterName, Object x) throws SQLException { + + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, int length) throws SQLException { + + } + + @Override + public void setDate(String parameterName, Date x, Calendar cal) throws SQLException { + + } + + @Override + public void setTime(String parameterName, Time x, Calendar cal) throws SQLException { + + } + + @Override + public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException { + + } + + @Override + public void setNull(String parameterName, int sqlType, String typeName) throws SQLException { + + } + + @Override + public String getString(String parameterName) throws SQLException { + return null; + } + + @Override + public boolean getBoolean(String parameterName) throws SQLException { + return false; + } + + @Override + public byte getByte(String parameterName) throws SQLException { + return 0; + } + + @Override + public short getShort(String parameterName) throws SQLException { + return 0; + } + + @Override + public int getInt(String parameterName) throws SQLException { + return 0; + } + + @Override + public long getLong(String parameterName) throws SQLException { + return 0; + } + + @Override + public float getFloat(String parameterName) throws SQLException { + return 0; + } + + @Override + public double getDouble(String parameterName) throws SQLException { + return 0; + } + + @Override + public byte[] getBytes(String parameterName) throws SQLException { + return new byte[0]; + } + + @Override + public Date getDate(String parameterName) throws SQLException { + return null; + } + + @Override + public Time getTime(String parameterName) throws SQLException { + return null; + } + + @Override + public Timestamp getTimestamp(String parameterName) throws SQLException { + return null; + } + + @Override + public Object getObject(String parameterName) throws SQLException { + return null; + } + + @Override + public BigDecimal getBigDecimal(String parameterName) throws SQLException { + return null; + } + + @Override + public Object getObject(String parameterName, Map> map) throws SQLException { + return null; + } + + @Override + public Ref getRef(String parameterName) throws SQLException { + return null; + } + + @Override + public Blob getBlob(String parameterName) throws SQLException { + return null; + } + + @Override + public Clob getClob(String parameterName) throws SQLException { + return null; + } + + @Override + public Array getArray(String parameterName) throws SQLException { + return null; + } + + @Override + public Date getDate(String parameterName, Calendar cal) throws SQLException { + return null; + } + + @Override + public Time getTime(String parameterName, Calendar cal) throws SQLException { + return null; + } + + @Override + public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException { + return null; + } + + @Override + public URL getURL(String parameterName) throws SQLException { + return null; + } + + @Override + public RowId getRowId(int parameterIndex) throws SQLException { + return null; + } + + @Override + public RowId getRowId(String parameterName) throws SQLException { + return null; + } + + @Override + public void setRowId(String parameterName, RowId x) throws SQLException { + + } + + @Override + public void setNString(String parameterName, String value) throws SQLException { + + } + + @Override + public void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException { + + } + + @Override + public void setNClob(String parameterName, NClob value) throws SQLException { + + } + + @Override + public void setClob(String parameterName, Reader reader, long length) throws SQLException { + + } + + @Override + public void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException { + + } + + @Override + public void setNClob(String parameterName, Reader reader, long length) throws SQLException { + + } + + @Override + public NClob getNClob(int parameterIndex) throws SQLException { + return null; + } + + @Override + public NClob getNClob(String parameterName) throws SQLException { + return null; + } + + @Override + public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException { + + } + + @Override + public SQLXML getSQLXML(int parameterIndex) throws SQLException { + return null; + } + + @Override + public SQLXML getSQLXML(String parameterName) throws SQLException { + return null; + } + + @Override + public String getNString(int parameterIndex) throws SQLException { + return null; + } + + @Override + public String getNString(String parameterName) throws SQLException { + return null; + } + + @Override + public Reader getNCharacterStream(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Reader getNCharacterStream(String parameterName) throws SQLException { + return null; + } + + @Override + public Reader getCharacterStream(int parameterIndex) throws SQLException { + return null; + } + + @Override + public Reader getCharacterStream(String parameterName) throws SQLException { + return null; + } + + @Override + public void setBlob(String parameterName, Blob x) throws SQLException { + + } + + @Override + public void setClob(String parameterName, Clob x) throws SQLException { + + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException { + + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException { + + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException { + + } + + @Override + public void setAsciiStream(String parameterName, InputStream x) throws SQLException { + + } + + @Override + public void setBinaryStream(String parameterName, InputStream x) throws SQLException { + + } + + @Override + public void setCharacterStream(String parameterName, Reader reader) throws SQLException { + + } + + @Override + public void setNCharacterStream(String parameterName, Reader value) throws SQLException { + + } + + @Override + public void setClob(String parameterName, Reader reader) throws SQLException { + + } + + @Override + public void setBlob(String parameterName, InputStream inputStream) throws SQLException { + + } + + @Override + public void setNClob(String parameterName, Reader reader) throws SQLException { + + } + + @Override + public T getObject(int parameterIndex, Class type) throws SQLException { + return null; + } + + @Override + public T getObject(String parameterName, Class type) throws SQLException { + return null; + } +} diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyPreparedStatementImpl.java b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyPreparedStatementImpl.java new file mode 100644 index 0000000..f3372a9 --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyPreparedStatementImpl.java @@ -0,0 +1,304 @@ +package software.amazon.disco.agent.integtest.sql.source; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; + +public class MyPreparedStatementImpl extends MyStatementImpl implements PreparedStatement { + public MyPreparedStatementImpl(Connection connection, ResultSet rs) { + super(connection, rs); + } + + @Override + public ResultSet executeQuery() throws SQLException { + return rs; + } + + @Override + public int executeUpdate() throws SQLException { + return 0; + } + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + + } + + @Override + public void setString(int parameterIndex, String x) throws SQLException { + + } + + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + + } + + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + + } + + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + + } + + @Override + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + + } + + @Override + public void clearParameters() throws SQLException { + + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + + } + + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + + } + + @Override + public boolean execute() throws SQLException { + return false; + } + + @Override + public void addBatch() throws SQLException { + + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + + } + + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + + } + + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + + } + + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + + } + + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return null; + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + + } + + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + + } + + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + return null; + } + + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + + } + + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + + } + + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + + } + + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + + } + + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + + } + + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + + } + + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + + } +} diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyStatementImpl.java b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyStatementImpl.java new file mode 100644 index 0000000..495a721 --- /dev/null +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/src/test/java/software/amazon/disco/agent/integtest/sql/source/MyStatementImpl.java @@ -0,0 +1,263 @@ +package software.amazon.disco.agent.integtest.sql.source; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; + +public class MyStatementImpl implements Statement { + public static final String QUERY = "SQL"; + private final Connection connection; + protected final ResultSet rs; // + + public MyStatementImpl(Connection connection, ResultSet rs) { + this.connection = connection; + this.rs = rs; + } + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + return rs; + } + + @Override + public int executeUpdate(String sql) throws SQLException { + return 0; + } + + @Override + public void close() throws SQLException { + + } + + @Override + public int getMaxFieldSize() throws SQLException { + return 0; + } + + @Override + public void setMaxFieldSize(int max) throws SQLException { + + } + + @Override + public int getMaxRows() throws SQLException { + return 0; + } + + @Override + public void setMaxRows(int max) throws SQLException { + + } + + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + + } + + @Override + public int getQueryTimeout() throws SQLException { + return 0; + } + + @Override + public void setQueryTimeout(int seconds) throws SQLException { + + } + + @Override + public void cancel() throws SQLException { + + } + + @Override + public SQLWarning getWarnings() throws SQLException { + return null; + } + + @Override + public void clearWarnings() throws SQLException { + + } + + @Override + public void setCursorName(String name) throws SQLException { + + } + + @Override + public boolean execute(String sql) throws SQLException { + return false; + } + + @Override + public ResultSet getResultSet() throws SQLException { + return null; + } + + @Override + public int getUpdateCount() throws SQLException { + return 0; + } + + @Override + public boolean getMoreResults() throws SQLException { + return false; + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + + } + + @Override + public int getFetchDirection() throws SQLException { + return 0; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + + } + + @Override + public int getFetchSize() throws SQLException { + return 0; + } + + @Override + public int getResultSetConcurrency() throws SQLException { + return 0; + } + + @Override + public int getResultSetType() throws SQLException { + return 0; + } + + @Override + public void addBatch(String sql) throws SQLException { + + } + + @Override + public void clearBatch() throws SQLException { + + } + + @Override + public int[] executeBatch() throws SQLException { + return new int[0]; + } + + @Override + public Connection getConnection() throws SQLException { + return connection; + } + + @Override + public boolean getMoreResults(int current) throws SQLException { + return false; + } + + @Override + public ResultSet getGeneratedKeys() throws SQLException { + return null; + } + + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + return 0; + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + return 0; + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + throw new SQLException(); + } + + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + return false; + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + return false; + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + return false; + } + + @Override + public int getResultSetHoldability() throws SQLException { + return 0; + } + + @Override + public boolean isClosed() throws SQLException { + return false; + } + + @Override + public void setPoolable(boolean poolable) throws SQLException { + + } + + @Override + public boolean isPoolable() throws SQLException { + return false; + } + + @Override + public void closeOnCompletion() throws SQLException { + + } + + @Override + public boolean isCloseOnCompletion() throws SQLException { + return false; + } + + @Override + public long executeLargeUpdate(String sql) throws SQLException { + return 0; + } + + @Override + public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + return 0; + } + + @Override + public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { + return 0; + } + + @Override + public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { + return 0; + } + + @Override + public T unwrap(Class iface) throws SQLException { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + + @Override + public String toString() { + return QUERY; + } +} diff --git a/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptor.java b/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptor.java index 2d83b90..f609589 100644 --- a/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptor.java +++ b/disco-java-agent-sql/src/main/java/software/amazon/disco/agent/sql/JdbcExecuteInterceptor.java @@ -42,8 +42,9 @@ * interface defines three methods to execute queries, which are explained more below. */ public class JdbcExecuteInterceptor implements Installable { - private static final Logger log = LogManager.getLogger(JdbcExecuteInterceptor.class); - static final String SQL_ORIGIN = "SQL"; + // Must be public for use in Advices + public static final Logger log = LogManager.getLogger(JdbcExecuteInterceptor.class); + public static final String SQL_ORIGIN = "SQL"; /** * This method is inlined at the beginning of all execute methods matched by {@link #buildMethodMatcher}. It @@ -62,7 +63,6 @@ public class JdbcExecuteInterceptor implements Installable { * @param stmt concrete statement class being used to make the query * @return a ServiceDownstreamRequestEvent with fields populated on a best effort basis */ - @SuppressWarnings("unused") @Advice.OnMethodEnter public static ServiceRequestEvent enter(@Advice.AllArguments final Object[] args, @Advice.Origin final String origin, @@ -105,7 +105,6 @@ public static ServiceRequestEvent enter(@Advice.AllArguments final Object[] args * @param thrown the Throwable thrown by the query, or null if query was successful. Passed in using the Thrown * annotation. Typically a {@link SQLException}. */ - @SuppressWarnings("unused") @Advice.OnMethodExit(onThrowable = Throwable.class) public static void exit(@Advice.Enter final ServiceRequestEvent requestEvent, @Advice.Return final Object response, From 786dc3f41adf2b514f68dfecedd45385ebcc1bac Mon Sep 17 00:00:00 2001 From: Connell Date: Fri, 7 Aug 2020 10:56:33 -0700 Subject: [PATCH 35/45] Tests could be flaky due to TransactionContext create/destroy mismatch in nearby tests. Ensured all tests have matched pairs and clean up after themselves --- .../UnableToInstrumentException.java | 33 ++++ .../export/JarModuleExportStrategy.java | 53 ++---- .../export/ModuleExportStrategy.java | 5 +- .../instrumentation/ModuleTransformer.java | 57 ++++--- .../TransformationListener.java | 21 ++- .../loaders/agents/DiscoAgentLoader.java | 2 - .../loaders/agents/TransformerExtractor.java | 102 +++++++++++ .../loaders/modules/JarModuleLoader.java | 119 ++++--------- .../loaders/modules/ModuleInfo.java | 4 +- .../preprocess/util/JarFileUtils.java | 57 +++++++ .../export/JarModuleExportStrategyTest.java | 26 +-- .../ModuleTransformerTest.java | 69 +++++--- .../TransformationListenerTest.java | 6 + .../loaders/agents/DiscoAgentLoaderTest.java | 43 +---- .../agents/TransformerExtractorTest.java | 42 +++++ .../loaders/modules/JarModuleLoaderTest.java | 159 ++++-------------- .../preprocess/util/JarFileTestUtils.java | 40 +++++ .../preprocess/util/MockEntities.java | 24 ++- .../apache/event/ApacheEventFactoryTests.java | 8 + .../ApacheHttpClientInterceptorTests.java | 2 +- 20 files changed, 503 insertions(+), 369 deletions(-) create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToInstrumentException.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarFileTestUtils.java diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToInstrumentException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToInstrumentException.java new file mode 100644 index 0000000..8da2409 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToInstrumentException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when the library fails to instrument a target class. + */ +public class UnableToInstrumentException extends RuntimeException { + /** + * Constructor accepting a message explaining the error as well as a {@link Throwable cause} + * + * @param message detailed message explaining the failure + * @param cause cause of the exception + */ + public UnableToInstrumentException(String message, Throwable cause) { + super(PreprocessConstants.MESSAGE_PREFIX + message, cause); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java index ebce64e..564abbc 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java @@ -17,6 +17,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleExportException; import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; @@ -44,41 +45,21 @@ public class JarModuleExportStrategy implements ModuleExportStrategy { private static final Logger log = LogManager.getLogger(JarModuleExportStrategy.class); private static Path tempDir = null; - private final String outputDir; - - /** - * Constructor with the destination path provided. - * - * @param outputDir Absolute output path of the transformed jar. Default is the current folder where - * the original jar is located. - */ - public JarModuleExportStrategy(final String outputDir) { - this.outputDir = outputDir; - } - - /** - * Constructor with no destination path provided. Transformed jar will replace the original file. - */ - public JarModuleExportStrategy() { - this.outputDir = null; - } /** * Exports all transformed classes to a Jar file. A temporary Jar File will be created to store all * the transformed classes and then be renamed to replace the original Jar. * - * @param moduleInfo Information of the original Jar - * @param instrumented a map of instrumented classes with their bytecode - * @param suffix suffix of the transformed package + * {@inheritDoc} */ @Override - public void export(final ModuleInfo moduleInfo, final Map instrumented, final String suffix) { + public void export(final ModuleInfo moduleInfo, final Map instrumented, final PreprocessConfig config) { log.debug(PreprocessConstants.MESSAGE_PREFIX + "Saving changes to Jar"); final File file = createTempFile(moduleInfo); buildOutputJar(moduleInfo, instrumented, file); - moveTempFileToDestination(moduleInfo, suffix, file); + moveTempFileToDestination(moduleInfo, config, file); } /** @@ -130,11 +111,11 @@ protected void saveTransformedClasses(final JarOutputStream jarOS, final Map instrumented) { - for (Enumeration entries = moduleInfo.getJarFile().entries(); entries.hasMoreElements(); ) { + protected void copyExistingJarEntries(final JarOutputStream jarOS, final JarFile jarFile, final Map instrumented) { + for (Enumeration entries = jarFile.entries(); entries.hasMoreElements(); ) { final JarEntry entry = (JarEntry) entries.nextElement(); final String keyToCheck = entry.getName().endsWith(".class") ? entry.getName().substring(0, entry.getName().lastIndexOf(".class")) : entry.getName(); @@ -143,7 +124,7 @@ protected void copyExistingJarEntries(final JarOutputStream jarOS, final ModuleI if (entry.isDirectory()) { jarOS.putNextEntry(entry); } else { - copyJarEntry(jarOS, moduleInfo.getJarFile(), entry); + copyJarEntry(jarOS, jarFile, entry); } } } catch (IOException e) { @@ -160,8 +141,8 @@ protected void copyExistingJarEntries(final JarOutputStream jarOS, final ModuleI * @param tempFile file that the JarOutputStream will write to */ protected void buildOutputJar(final ModuleInfo moduleInfo, final Map instrumented, final File tempFile) { - try (JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(tempFile))) { - copyExistingJarEntries(jarOS, moduleInfo, instrumented); + try (JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(tempFile)); JarFile jarFile = new JarFile(moduleInfo.getFile())) { + copyExistingJarEntries(jarOS, jarFile, instrumented); saveTransformedClasses(jarOS, instrumented); } catch (IOException e) { throw new ModuleExportException("Failed to create output Jar file", e); @@ -201,22 +182,22 @@ protected void copyJarEntry(final JarOutputStream jarOS, final JarFile file, fin /** * Move the temp file containing all existing entries and transformed classes to the - * destination. If {@link #outputDir} is NOT specified, the original file will be replaced. + * destination. If {@link PreprocessConfig#getOutputDir()} is null, the original file will be replaced. * * @param moduleInfo Information of the original Jar - * @param suffix suffix to be appended to the transformed package + * @param config configuration file containing instructions to instrument a module * @param tempFile output file to be moved to the destination path * @return {@link Path} of the overwritten file */ - protected Path moveTempFileToDestination(final ModuleInfo moduleInfo, final String suffix, final File tempFile) { + protected Path moveTempFileToDestination(final ModuleInfo moduleInfo, final PreprocessConfig config, final File tempFile) { try { - final String destinationStr = outputDir == null ? + final String destinationStr = config.getOutputDir() == null ? moduleInfo.getFile().getAbsolutePath() - : outputDir + "/" + moduleInfo.getFile().getName(); + : config.getOutputDir() + "/" + moduleInfo.getFile().getName(); - final String destinationStrWithSuffix = suffix == null ? + final String destinationStrWithSuffix = config.getSuffix() == null ? destinationStr - : destinationStr.substring(0, destinationStr.lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + suffix + PreprocessConstants.JAR_EXTENSION; + : destinationStr.substring(0, destinationStr.lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + config.getSuffix() + PreprocessConstants.JAR_EXTENSION; final Path destination = Paths.get(destinationStrWithSuffix); destination.toFile().getParentFile().mkdirs(); diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java index b2e0b78..833bae8 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java @@ -15,6 +15,7 @@ package software.amazon.disco.instrumentation.preprocess.export; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; @@ -29,7 +30,7 @@ public interface ModuleExportStrategy { * * @param info Information of the original Jar * @param instrumented a map of instrumented classes with their bytecode - * @param suffix suffix to be appended to the transformed package + * @param config configuration file containing instructions to instrument a module */ - void export(final ModuleInfo info, final Map instrumented, final String suffix); + void export(final ModuleInfo info, final Map instrumented, final PreprocessConfig config); } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java index d2357df..19a128d 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java @@ -21,17 +21,20 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator; -import software.amazon.disco.agent.inject.Injector; import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.AgentLoaderNotProvidedException; import software.amazon.disco.instrumentation.preprocess.exceptions.InvalidConfigEntryException; import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleLoaderNotProvidedException; +import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToInstrumentException; import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.loaders.agents.AgentLoader; +import software.amazon.disco.instrumentation.preprocess.loaders.agents.TransformerExtractor; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleLoader; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; import java.util.Map; /** @@ -56,29 +59,30 @@ public class ModuleTransformer { * and trigger the program to exit with status 1 */ public void transform() { - try { - if (config == null) {throw new InvalidConfigEntryException("No configuration provided", null);} + if (config == null || config.getLogLevel() == null) { + Configurator.setRootLevel(Level.INFO); + } else { + Configurator.setRootLevel(config.getLogLevel()); + } - if (config.getLogLevel() == null) { - Configurator.setRootLevel(Level.INFO); - } else { - Configurator.setRootLevel(config.getLogLevel()); + try { + if (config == null) { + throw new InvalidConfigEntryException("No configuration provided", null); + } + if (agentLoader == null) { + throw new AgentLoaderNotProvidedException(); } - - if (agentLoader == null) {throw new AgentLoaderNotProvidedException();} - if (jarLoader == null) {throw new ModuleLoaderNotProvidedException();} - - agentLoader.loadAgent(config, Injector.createInstrumentation()); - if (jarLoader == null) { throw new ModuleLoaderNotProvidedException(); } - // Apply instrumentation on all jars + agentLoader.loadAgent(config, new TransformerExtractor()); + + // Apply instrumentation on all loaded jars for (final ModuleInfo info : jarLoader.loadPackages(config)) { applyInstrumentation(info); - //todo: store serialized instrumentation state to target jar } + } catch (RuntimeException e) { log.error(e); System.exit(1); @@ -86,22 +90,25 @@ public void transform() { } /** - * Triggers instrumentation of classes using Reflection and applies the changes according to the - * {@link ModuleExportStrategy export strategy} - * of this package + * Triggers instrumentation of classes using Reflection and applies and saves the changes according to the provided + * {@link ModuleExportStrategy export strategy} on a local file * - * @param moduleInfo a package containing classes to be instrumented + * @param moduleInfo meta-data of a loaded jar file */ - protected void applyInstrumentation(final ModuleInfo moduleInfo) { - for (String name : moduleInfo.getClassNames()) { + protected void applyInstrumentation(ModuleInfo moduleInfo) { + log.info("applying transformation on: " + moduleInfo.getFile().getAbsolutePath()); + for (Map.Entry entry : moduleInfo.getClassByteCodeMap().entrySet()) { try { - Class.forName(name); - } catch (ClassNotFoundException | NoClassDefFoundError e) { - log.warn(PreprocessConstants.MESSAGE_PREFIX + "Failed to initialize class:" + name, e); + for (ClassFileTransformer transformer : TransformerExtractor.getTransformers()) { + transformer.transform(getClass().getClassLoader(), entry.getKey(), null, null, entry.getValue()); + } + } catch (IllegalClassFormatException e) { + throw new UnableToInstrumentException("Failed to instrument : " + entry.getKey(), e); } } - moduleInfo.getExportStrategy().export(moduleInfo, getInstrumentedClasses(), config.getSuffix()); + log.debug(PreprocessConstants.MESSAGE_PREFIX + getInstrumentedClasses().size() + " classes transformed"); + moduleInfo.getExportStrategy().export(moduleInfo, getInstrumentedClasses(), config); // empty the map in preparation for transforming another package getInstrumentedClasses().clear(); diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java index 1f83a5a..ed5098a 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package software.amazon.disco.instrumentation.preprocess.instrumentation; import lombok.Getter; @@ -7,7 +22,7 @@ import net.bytebuddy.utility.JavaModule; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; +import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToInstrumentException; import java.util.HashMap; import java.util.Map; @@ -58,7 +73,7 @@ public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, * {@inheritDoc} */ public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) { - log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to instrument: " + typeName, throwable); + throw new UnableToInstrumentException("Failed to instrument : " + typeName, throwable); } /** @@ -82,7 +97,7 @@ protected void collectDataFromEvent(TypeDescription typeDescription, DynamicType } if (!dynamicType.getAuxiliaryTypes().isEmpty()) { - for(Map.Entry auxiliaryEntry : dynamicType.getAuxiliaryTypes().entrySet()){ + for (Map.Entry auxiliaryEntry : dynamicType.getAuxiliaryTypes().entrySet()) { instrumentedTypes.put(auxiliaryEntry.getKey().getInternalName(), new InstrumentedClassState(null, auxiliaryEntry.getValue())); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java index fa43aa0..d2cb083 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java @@ -51,8 +51,6 @@ public void loadAgent(final PreprocessConfig config, Instrumentation instrumenta throw new NoAgentToLoadException(); } - instrumentation = instrumentation == null ? Injector.createInstrumentation() : instrumentation; - final ClassFileVersion version = parseClassFileVersionFromConfig(config); DiscoAgentTemplate.setAgentConfigFactory(() -> { diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java new file mode 100644 index 0000000..7913f34 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java @@ -0,0 +1,102 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.agents; + +import lombok.Getter; +import software.amazon.disco.agent.inject.Injector; + +import java.lang.instrument.ClassDefinition; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarFile; + +/** + * A No-op implementation of the {@link Instrumentation} interface that extracts all {@link ClassFileTransformer transformers} installed + * onto the instance and appends the agent jar to the bootstrap class path. + */ +public class TransformerExtractor implements Instrumentation { + @Getter + private static List transformers = new ArrayList<>(); + + @Override + public void addTransformer(ClassFileTransformer transformer, boolean canRetransform) { + transformers.add(transformer); + } + + @Override + public void addTransformer(ClassFileTransformer transformer) { + transformers.add(transformer); + } + + @Override + public boolean removeTransformer(ClassFileTransformer transformer) { + return false; + } + + @Override + public boolean isRetransformClassesSupported() { + return true; + } + + @Override + public void retransformClasses(Class... classes) { } + + @Override + public boolean isRedefineClassesSupported() { + return true; + } + + @Override + public void redefineClasses(ClassDefinition... definitions) { } + + @Override + public boolean isModifiableClass(Class theClass) { + return false; + } + + @Override + public Class[] getAllLoadedClasses() { + return new Class[0]; + } + + @Override + public Class[] getInitiatedClasses(ClassLoader loader) { + return new Class[0]; + } + + @Override + public long getObjectSize(Object objectToSize) { + return 0; + } + + @Override + public void appendToBootstrapClassLoaderSearch(JarFile jarfile) { + Injector.createInstrumentation().appendToBootstrapClassLoaderSearch(jarfile); + } + + @Override + public void appendToSystemClassLoaderSearch(JarFile jarfile) { } + + @Override + public boolean isNativeMethodPrefixSupported() { + return false; + } + + @Override + public void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix) { } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java index 9cd3368..ee5e964 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java @@ -15,22 +15,23 @@ package software.amazon.disco.instrumentation.preprocess.loaders.modules; -import lombok.Getter; -import software.amazon.disco.agent.inject.Injector; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import software.amazon.disco.agent.inject.Injector; import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.util.JarFileUtils; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Enumeration; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -40,30 +41,13 @@ public class JarModuleLoader implements ModuleLoader { private static final Logger log = LogManager.getLogger(JarModuleLoader.class); - @Getter - private final ModuleExportStrategy strategy; - - /** - * Default constructor that sets {@link #strategy} to {@link JarModuleExportStrategy} - */ - public JarModuleLoader() { - this.strategy = new JarModuleExportStrategy(); - } - - /** - * Constructor accepting a custom export strategy - * - * @param strategy {@link ModuleExportStrategy strategy} for exporting transformed classes under this path. Default strategy is {@link JarModuleExportStrategy} - */ - public JarModuleLoader(final ModuleExportStrategy strategy) { - this.strategy = strategy; - } - /** * {@inheritDoc} */ @Override public List loadPackages(PreprocessConfig config) { + log.info(PreprocessConstants.MESSAGE_PREFIX + "Loading packages"); + if (config == null || config.getJarPaths() == null) { throw new NoModuleToInstrumentException(); } @@ -71,14 +55,13 @@ public List loadPackages(PreprocessConfig config) { final List packageEntries = new ArrayList<>(); for (String path : config.getJarPaths()) { - for (File file : discoverFilesInPath(path)) { - final ModuleInfo info = loadPackage(file); + final ModuleInfo info = loadPackage(new File(path), new JarModuleExportStrategy()); - if (info != null) { - packageEntries.add(info); - } + if (info != null) { + packageEntries.add(info); } } + if (packageEntries.isEmpty()) { throw new NoModuleToInstrumentException(); } @@ -86,43 +69,39 @@ public List loadPackages(PreprocessConfig config) { } /** - * Discovers all files under a path + * Helper method to load one single Jar file * - * @return a List of {@link File files}, empty is no files found + * @param file Jar file to be loaded + * @return {@link ModuleInfo object} containing package data, null if file is not a valid {@link JarFile} */ - protected List discoverFilesInPath(final String path) { - final List files = new ArrayList<>(); - final File packageDir = new File(path); + protected ModuleInfo loadPackage(final File file, final ModuleExportStrategy strategy) { + log.info(PreprocessConstants.MESSAGE_PREFIX + "Loading module: " + file.getAbsolutePath()); - final File[] packageFiles = packageDir.listFiles(); + try (JarFile jarFile = new JarFile(file)) { + final Map types = new HashMap<>(); - if (packageFiles == null) { - log.debug(PreprocessConstants.MESSAGE_PREFIX + "No packages found under path: " + path); - return files; - } + if (jarFile == null) { + log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to load module: "+file.getAbsolutePath()); + return null; + } - files.addAll(Arrays.asList(packageFiles)); - return files; - } + injectFileToSystemClassPath(file); - /** - * Helper method to load one single Jar file - * - * @param file Jar file to be loaded - * @return {@link ModuleInfo object} containing package data, null if file is not a valid {@link JarFile} - */ - protected ModuleInfo loadPackage(final File file) { - final JarFile jarFile = processFile(file); + log.info(PreprocessConstants.MESSAGE_PREFIX + "Module loaded"); - if (jarFile == null) return null; + for (JarEntry entry : extractEntries(jarFile)) { + if (entry.getName().endsWith(".class")) { + final String nameWithoutExtension = entry.getName().substring(0, entry.getName().lastIndexOf(".class")).replace('/', '.'); - final List names = new ArrayList<>(); + types.put(nameWithoutExtension, JarFileUtils.readEntryFromJar(jarFile, entry)); + } + } - for (JarEntry entry : extractEntries(jarFile)) { - names.add(entry.getName().substring(0, entry.getName().lastIndexOf(".class")).replace('/', '.')); + return types.isEmpty() ? null : new ModuleInfo(file, strategy, types); + } catch (IOException e) { + log.error(PreprocessConstants.MESSAGE_PREFIX + "Invalid file, skipped", e); + return null; } - - return names.isEmpty() ? null : new ModuleInfo(file, jarFile, names, strategy); } /** @@ -148,38 +127,6 @@ protected List extractEntries(final JarFile jarFile) { return result; } - /** - * Validates the file and adds it to the system class path - * - * @param file file to process - * @return a valid {@link JarFile}, null if Jar cannot be created from {@link File} passed in. - */ - protected JarFile processFile(final File file) { - if (file.isDirectory() || !file.getName().toLowerCase().contains(PreprocessConstants.JAR_EXTENSION)) return null; - - final JarFile jar = makeJarFile(file); - - if (jar != null) { - injectFileToSystemClassPath(file); - } - return jar; - } - - /** - * Creates a {@link JarFile} from {@link File}. - * - * @param file file to construct the Jar file from - * @return a valid {@link JarFile}, null if invalid - */ - protected JarFile makeJarFile(final File file) { - try { - return new JarFile(file); - } catch (IOException e) { - log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to create JarFile from file: " + file.getName(), e); - return null; - } - } - /** * Add the file to the system class path using the {@link Injector injector} api. * diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java index 999e9c4..fd54f1e 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java @@ -21,6 +21,7 @@ import java.io.File; import java.util.List; +import java.util.Map; import java.util.jar.JarFile; /** @@ -31,7 +32,6 @@ @Getter public class ModuleInfo { private final File file; - private final JarFile jarFile; - private final List classNames; private final ModuleExportStrategy exportStrategy; + private final Map classByteCodeMap; } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java new file mode 100644 index 0000000..1e987f6 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.util; + +import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Utility class for performing JarFile related tasks. + */ +public class JarFileUtils { + + /** + * Reads the byte[] of a JarEntry from a JarFile + * + * @param jarfile JarFile where the binary data will be read + * @param entry JarEntry to be read + * @return byte[] of the entry + * @throws UnableToReadJarEntryException + */ + public static byte[] readEntryFromJar(JarFile jarfile, JarEntry entry) { + try (final InputStream entryStream = jarfile.getInputStream(entry)) { + if (entryStream == null) { + throw new UnableToReadJarEntryException(entry.getName(), null); + } + + final byte[] buffer = new byte[2048]; + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + for (int len = entryStream.read(buffer); len != -1; len = entryStream.read(buffer)) { + os.write(buffer, 0, len); + } + return os.toByteArray(); + + } catch (IOException e) { + throw new UnableToReadJarEntryException(entry.getName(), null); + } + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java index b6c2485..78d500b 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java @@ -22,7 +22,7 @@ import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; -import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleExportException; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; @@ -54,6 +54,7 @@ public class JarModuleExportStrategyTest { JarOutputStream mockJarOS; JarModuleExportStrategy mockStrategy; JarModuleExportStrategy spyStrategy; + PreprocessConfig config; @Before public void before() throws IOException { @@ -63,7 +64,9 @@ public void before() throws IOException { mockJarOS = Mockito.mock(JarOutputStream.class); spyStrategy = Mockito.spy(new JarModuleExportStrategy()); - mockModuleInfo = MockEntities.makeMockPackageInfo(); + mockModuleInfo = MockEntities.makeMockModuleInfo(); + config = PreprocessConfig.builder().build(); + Mockito.doCallRealMethod().when(mockStrategy).export(Mockito.any(), Mockito.any(), Mockito.any()); Mockito.when(mockStrategy.createTempFile(Mockito.any())).thenReturn(tempFolder.newFile(TEMP_FILE_NAME)); } @@ -103,9 +106,10 @@ public void testSaveTransformedClassesWorksAndCreatesNewEntries() throws IOExcep @Test public void testCopyExistingJarEntriesWorksWithFilesAndPath() throws IOException { + JarFile jarFile = MockEntities.makeMockJarFile(); Mockito.doCallRealMethod().when(mockStrategy).copyExistingJarEntries(Mockito.eq(mockJarOS), Mockito.any(), Mockito.any()); - mockStrategy.copyExistingJarEntries(mockJarOS, mockModuleInfo, MockEntities.makeInstrumentedClassesMap()); + mockStrategy.copyExistingJarEntries(mockJarOS, jarFile, MockEntities.makeInstrumentedClassesMap()); // 3 out of 6 classes have not been instrumented Mockito.verify(mockStrategy, Mockito.times(3)).copyJarEntry(Mockito.eq(mockJarOS), Mockito.any(), Mockito.any()); @@ -150,7 +154,7 @@ public void testMoveTempFileToDestinationToReplaceOriginal() throws IOException File file = spyStrategy.createTempFile(mockModuleInfo); // replace original file - Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, null, file); + Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, config, file); Assert.assertNotEquals(originalLength, path.toFile().length()); Assert.assertEquals(originalFile.getAbsolutePath(), path.toFile().getAbsolutePath()); @@ -161,7 +165,8 @@ public void testMoveTempFileToDestinationToReplaceOriginal() throws IOException @Test public void testMoveTempFileToDestinationWorks() throws IOException { File outDir = tempFolder.newFolder(OUT_DIR); - spyStrategy = new JarModuleExportStrategy(outDir.getAbsolutePath()); + config = PreprocessConfig.builder().outputDir(outDir.getAbsolutePath()).build(); + JarModuleExportStrategy spyStrategy = new JarModuleExportStrategy(); // create original file and assume temp/disco/tests is where the original package is File originalFile = createOriginalFile(); @@ -172,7 +177,7 @@ public void testMoveTempFileToDestinationWorks() throws IOException { File file = spyStrategy.createTempFile(mockModuleInfo); // move to destination - Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, null, file); + Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, config, file); Assert.assertEquals(outDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); Assert.assertEquals(mockModuleInfo.getFile().getName(), path.toFile().getName()); @@ -183,8 +188,9 @@ public void testMoveTempFileToDestinationWorks() throws IOException { @Test public void testMoveTempFileToDestinationWorksWithSuffix() throws IOException { - File outputDir = tempFolder.newFolder(OUT_DIR); - spyStrategy = new JarModuleExportStrategy(outputDir.getAbsolutePath()); + File outDir = tempFolder.newFolder(OUT_DIR); + config = PreprocessConfig.builder().suffix(PACKAGE_SUFFIX).outputDir(outDir.getAbsolutePath()).build(); + spyStrategy = new JarModuleExportStrategy(); // create original file and assume temp/disco/tests is where the original package is File originalFile = createOriginalFile(); @@ -193,7 +199,7 @@ public void testMoveTempFileToDestinationWorksWithSuffix() throws IOException { // move to destination Mockito.when(mockModuleInfo.getFile()).thenReturn(originalFile); - Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, PACKAGE_SUFFIX, tempFile); + Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, config, tempFile); String nameToCheck = mockModuleInfo.getFile() .getName() @@ -201,7 +207,7 @@ public void testMoveTempFileToDestinationWorksWithSuffix() throws IOException { + PACKAGE_SUFFIX + PreprocessConstants.JAR_EXTENSION; - Assert.assertEquals(outputDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); + Assert.assertEquals(outDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); Assert.assertEquals(nameToCheck, path.toFile().getName()); Assert.assertTrue(originalFile.exists()); } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java index a54ac0f..e240a53 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java @@ -17,6 +17,7 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -27,14 +28,16 @@ import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.loaders.agents.DiscoAgentLoader; +import software.amazon.disco.instrumentation.preprocess.loaders.agents.TransformerExtractor; import software.amazon.disco.instrumentation.preprocess.loaders.modules.JarModuleLoader; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; import software.amazon.disco.instrumentation.preprocess.util.MockEntities; -import java.lang.instrument.Instrumentation; -import java.util.ArrayList; +import java.io.File; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; import java.util.Arrays; -import java.util.List; +import java.util.HashMap; import java.util.Map; @RunWith(MockitoJUnitRunner.class) @@ -42,6 +45,7 @@ public class ModuleTransformerTest { private static final String PACKAGE_SUFFIX = "suffix"; ModuleTransformer spyTransformer; + PreprocessConfig config; @Mock DiscoAgentLoader mockAgentLoader; @@ -49,8 +53,8 @@ public class ModuleTransformerTest { @Mock JarModuleLoader mockJarPackageLoader; - PreprocessConfig config; - List moduleInfos; + @Mock + ModuleInfo moduleInfo; @Before public void before() { @@ -68,19 +72,13 @@ public void before() { .build() ); - Mockito.doReturn(Arrays.asList(MockEntities.makeMockPackageInfo())) + Mockito.doReturn(Arrays.asList(MockEntities.makeMockModuleInfo())) .when(mockJarPackageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); - - moduleInfos = new ArrayList<>(); - moduleInfos.add(Mockito.mock(ModuleInfo.class)); - moduleInfos.add(Mockito.mock(ModuleInfo.class)); } - @Test - public void testTransformWorksWithDefaultLogLevel() { - spyTransformer.transform(); - - Assert.assertEquals(LogManager.getLogger().getLevel(), Level.INFO); + @After + public void after(){ + TransformerExtractor.getTransformers().clear(); } @Test @@ -100,6 +98,12 @@ public void testTransformWorksWithVerboseLogLevel() { Assert.assertEquals(Level.TRACE, LogManager.getLogger().getLevel()); } + @Test + public void testTransformWorksWithDefaultLogLevel() { + spyTransformer.transform(); + Assert.assertEquals(LogManager.getLogger().getLevel(), Level.INFO); + } + @Test public void testTransformWorksAndInvokesLoadAgentAndPackages() { spyTransformer = Mockito.spy( @@ -111,7 +115,7 @@ public void testTransformWorksAndInvokesLoadAgentAndPackages() { ); spyTransformer.transform(); - Mockito.verify(mockAgentLoader).loadAgent(Mockito.any(PreprocessConfig.class), Mockito.any(Instrumentation.class)); + Mockito.verify(mockAgentLoader).loadAgent(Mockito.any(PreprocessConfig.class), Mockito.any(TransformerExtractor.class)); Mockito.verify(mockJarPackageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); } @@ -123,21 +127,34 @@ public void testTransformWorksAndInvokesPackageLoader() { Mockito.verify(spyTransformer).applyInstrumentation(Mockito.any()); } - @Test - public void testApplyInstrumentationWorksAndInvokesExport() { - Mockito.doCallRealMethod().when(spyTransformer).applyInstrumentation(Mockito.any()); - - JarModuleExportStrategy s1 = Mockito.mock(JarModuleExportStrategy.class); - Mockito.when(moduleInfos.get(0).getExportStrategy()).thenReturn(s1); - + public void testApplyInstrumentationWorks() throws IllegalClassFormatException { + JarModuleExportStrategy strategy = Mockito.mock(JarModuleExportStrategy.class); Map instrumentedClasses = MockEntities.makeInstrumentedClassesMap(); + File file = Mockito.mock(File.class); + + Map byteArrayMap = new HashMap<>(); + byteArrayMap.put("ClassA", new byte[]{1}); + byteArrayMap.put("ClassB", new byte[]{2}); + + TransformerExtractor transformerExtractor = new TransformerExtractor(); + ClassFileTransformer transformer_1 = Mockito.mock(ClassFileTransformer.class); + ClassFileTransformer transformer_2 = Mockito.mock(ClassFileTransformer.class); + transformerExtractor.addTransformer(transformer_1); + transformerExtractor.addTransformer(transformer_2); + + Mockito.when(moduleInfo.getExportStrategy()).thenReturn(strategy); + Mockito.when(moduleInfo.getClassByteCodeMap()).thenReturn(byteArrayMap); + Mockito.when(moduleInfo.getFile()).thenReturn(file); + Mockito.when(file.getAbsolutePath()).thenReturn("mock/path"); Mockito.doReturn(instrumentedClasses).when(spyTransformer).getInstrumentedClasses(); - spyTransformer.applyInstrumentation(moduleInfos.get(0)); + spyTransformer.applyInstrumentation(moduleInfo); - Mockito.verify(moduleInfos.get(0)).getClassNames(); - Mockito.verify(s1).export(moduleInfos.get(0), instrumentedClasses, PACKAGE_SUFFIX); + Mockito.verify(moduleInfo).getClassByteCodeMap(); + Mockito.verify(strategy).export(moduleInfo, instrumentedClasses, config); + Mockito.verify(transformer_1).transform(Mockito.any(ClassLoader.class), Mockito.eq("ClassA"), Mockito.eq(null), Mockito.eq(null), Mockito.eq(new byte[]{1})); + Mockito.verify(transformer_1).transform(Mockito.any(ClassLoader.class), Mockito.eq("ClassB"), Mockito.eq(null), Mockito.eq(null), Mockito.eq(new byte[]{2})); Assert.assertTrue(instrumentedClasses.isEmpty()); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java index b05462e..159286a 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java @@ -2,6 +2,7 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.DynamicType; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -28,6 +29,11 @@ public void before() { Mockito.when(mockTypeDescription.getInternalName()).thenReturn(MockEntities.makeClassPaths().get(0)); } + @After + public void after() { + TransformationListener.getInstrumentedTypes().clear(); + } + @Test public void testOnTransformationWorksAndInvokesCollectDataFromEvent() { Mockito.doCallRealMethod().when(mockListener).onTransformation(mockTypeDescription, null, null, false, mockDynamicTypeWithAuxiliary); diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java index 4aa5cb1..4227ff4 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java @@ -30,14 +30,12 @@ import software.amazon.disco.instrumentation.preprocess.exceptions.InvalidConfigEntryException; import software.amazon.disco.instrumentation.preprocess.exceptions.NoAgentToLoadException; import software.amazon.disco.instrumentation.preprocess.instrumentation.TransformationListener; +import software.amazon.disco.instrumentation.preprocess.util.JarFileTestUtils; import java.io.File; -import java.io.FileOutputStream; import java.lang.instrument.Instrumentation; import java.util.function.Supplier; import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; -import java.util.zip.ZipEntry; public class DiscoAgentLoaderTest { @Rule @@ -49,7 +47,7 @@ public void testLoadAgentFailOnNullPaths() throws NoAgentToLoadException { } @Test - public void testParsingJavaVersionWorks(){ + public void testParsingJavaVersionWorks() { PreprocessConfig config = PreprocessConfig.builder() .agentPath("path") .javaVersion("11") @@ -66,7 +64,7 @@ public void testParsingJavaVersionWorks(){ } @Test(expected = InvalidConfigEntryException.class) - public void testParsingJavaVersionFailsWithInvalidJavaVersion(){ + public void testParsingJavaVersionFailsWithInvalidJavaVersion() { PreprocessConfig config = PreprocessConfig.builder() .agentPath("path") .javaVersion("a version") @@ -76,11 +74,11 @@ public void testParsingJavaVersionFailsWithInvalidJavaVersion(){ @Test public void testLoadAgentRegistersAgentBuilderTransformerAndInstallsAgent() throws Exception { - Instrumentation instrumentation = Mockito.mock(Instrumentation.class); + Instrumentation instrumentation = Mockito.spy(new TransformerExtractor()); AgentBuilder agentBuilder = Mockito.mock(AgentBuilder.class); Mockito.when(agentBuilder.with(Mockito.any(ByteBuddy.class))).thenReturn(agentBuilder); - File file = createJar("TestJarFile"); + File file = JarFileTestUtils.createJar(temporaryFolder, "TestJarFile", "Foo.class"); PreprocessConfig config = PreprocessConfig.builder().agentPath(file.getAbsolutePath()).build(); Assert.assertNull(DiscoAgentTemplate.getAgentConfigFactory()); @@ -95,8 +93,7 @@ public void testLoadAgentRegistersAgentBuilderTransformerAndInstallsAgent() thro agentConfigSupplier.get().getAgentBuilderTransformer().apply(agentBuilder, null); Mockito.verify(agentBuilder).with(Mockito.any(TransformationListener.class)); - // check if a ByteBuddy instance with the correct java version is being installed using its own - // equals method + // check if a ByteBuddy instance with the correct java version is being installed using its own equals method Assert.assertEquals(ClassFileVersion.JAVA_V8, DiscoAgentLoader.parseClassFileVersionFromConfig(config)); ArgumentCaptor byteBuddyArgumentCaptor = ArgumentCaptor.forClass(ByteBuddy.class); Mockito.verify(agentBuilder).with(byteBuddyArgumentCaptor.capture()); @@ -107,32 +104,4 @@ public void testLoadAgentRegistersAgentBuilderTransformerAndInstallsAgent() thro Mockito.verify(instrumentation).appendToBootstrapClassLoaderSearch(jarFileArgumentCaptor.capture()); Assert.assertEquals(file.getAbsolutePath(), jarFileArgumentCaptor.getValue().getName()); } - - private File createJar(String name) throws Exception { - File file = temporaryFolder.newFile(name+".jar"); - try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { - try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { - //write a sentinal file with the same name as the jar, to test if it becomes readable by getResource. - jarOutputStream.putNextEntry(new ZipEntry(name)); - jarOutputStream.write("foobar".getBytes()); - jarOutputStream.closeEntry(); - } - } - return file; - } - -// class MockAgentBuilderTransformer implements BiFunction { -// @Override -// public AgentBuilder apply(AgentBuilder agentBuilder, Installable installable) { -// return agentBuilder -// .with(new ByteBuddy(version)) -// .with(new TransformationListener(uuidGenerate(installable))); -// } -// -// class ByteBuddyTest extends ByteBuddy{ -// public ClassFileVersion getClassFileVersion(){ -// return classFileVersion; -// } -// } -// } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java new file mode 100644 index 0000000..4425e49 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.agents; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.lang.instrument.ClassFileTransformer; + +public class TransformerExtractorTest { + @After + public void after(){ + TransformerExtractor.getTransformers().clear(); + } + + @Test + public void testAddTransformerWorks(){ + ClassFileTransformer classFileTransformer = Mockito.mock(ClassFileTransformer.class); + + TransformerExtractor extractor = new TransformerExtractor(); + extractor.addTransformer(classFileTransformer); + extractor.addTransformer(classFileTransformer); + extractor.addTransformer(classFileTransformer); + + Assert.assertEquals(3, TransformerExtractor.getTransformers().size()); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java index 2d660d0..1a1c5e4 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java @@ -17,177 +17,86 @@ import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; +import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; -import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.util.JarFileTestUtils; import software.amazon.disco.instrumentation.preprocess.util.MockEntities; import java.io.File; -import java.util.Arrays; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.stream.Collectors; -@RunWith(MockitoJUnitRunner.class) public class JarModuleLoaderTest { - static final List PATHS = MockEntities.makeMockPathsWithDuplicates(); - static final List MOCK_FILES = MockEntities.makeMockFiles(); - static final List MOCK_JAR_ENTRIES = MockEntities.makeMockJarEntries(); - JarModuleLoader loader; PreprocessConfig config; - @Mock - JarFile jarFile; - - @Mock - File mockFile; + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Before - public void before(){ - config = PreprocessConfig.builder().jarPaths(PATHS).build(); + public void before() { + config = PreprocessConfig.builder().jarPaths(MockEntities.makeMockPathsWithDuplicates()).build(); loader = new JarModuleLoader(); - - Mockito.when(mockFile.isDirectory()).thenReturn(false); - Mockito.when(mockFile.getName()).thenReturn("ATestJar.jar"); } @Test(expected = NoModuleToInstrumentException.class) - public void testConstructorFailWithEmptyPathList() { - new JarModuleLoader().loadPackages(config); + public void testLoadPackagesFailWithEmptyPathList() { + loader.loadPackages(config); } @Test(expected = NoModuleToInstrumentException.class) - public void testConstructorFailWithNullConfig() { - new JarModuleLoader().loadPackages(null); + public void testLoadPackagesFailWithNullConfig() { + loader.loadPackages(null); } @Test(expected = NoModuleToInstrumentException.class) - public void testConstructorFailWithNullPathList() { - new JarModuleLoader().loadPackages(PreprocessConfig.builder().build()); - } - - @Test - public void testConstructorWorksAndHasDefaultStrategy() { - Assert.assertTrue(loader.getStrategy().getClass().equals(JarModuleExportStrategy.class)); - } - - @Test - public void testConstructorWorksWithNonDefaultStrategy() { - ModuleExportStrategy mockStrategy = Mockito.mock(ModuleExportStrategy.class); - - loader = new JarModuleLoader(mockStrategy); - Assert.assertNotEquals(JarModuleExportStrategy.class, loader.getStrategy().getClass()); - } - - @Test - public void testProcessFileWorksWithValidFileExtension(){ - JarModuleLoader loader = Mockito.mock(JarModuleLoader.class); - JarFile jar = Mockito.mock(JarFile.class); - - Mockito.doCallRealMethod().when(loader).processFile(mockFile); - Mockito.doReturn(jar).when(loader).makeJarFile(mockFile); - - Assert.assertNotNull(loader.processFile(mockFile)); - - Mockito.when(mockFile.getName()).thenReturn("ATestJar.JAR"); - Assert.assertNotNull(loader.processFile(mockFile)); - } - - @Test - public void testProcessFileWorksWithInvalidFileExtensionAndReturnNull(){ - JarModuleLoader loader = Mockito.mock(JarModuleLoader.class); - - Mockito.when(mockFile.getName()).thenReturn("ATestJar.txt"); - Mockito.doCallRealMethod().when(loader).processFile(mockFile); - - Assert.assertNull(loader.processFile(mockFile)); + public void testLoadPackagesFailWithNullPathList() { + loader.loadPackages(PreprocessConfig.builder().build()); } @Test - public void testProcessFileWorksAndInvokesInjectFileToSystemClassPath() { + public void testLoadPackagesWorksWithMultiplePaths() { JarModuleLoader packageLoader = Mockito.mock(JarModuleLoader.class); - Mockito.when(packageLoader.processFile(Mockito.any())).thenCallRealMethod(); + ModuleInfo info = Mockito.mock(ModuleInfo.class); - JarFile mockJarfile = Mockito.mock(JarFile.class); - Mockito.when(packageLoader.makeJarFile(mockFile)).thenReturn(mockJarfile); + Mockito.doCallRealMethod().when(packageLoader).loadPackages(config); + Mockito.doReturn(info).when(packageLoader).loadPackage(Mockito.any(File.class), Mockito.any(ModuleExportStrategy.class)); - packageLoader.processFile(mockFile); + List infos = packageLoader.loadPackages(config); - Mockito.verify(packageLoader).injectFileToSystemClassPath(mockFile); + Mockito.verify(packageLoader, Mockito.times(3)).loadPackage(Mockito.any(File.class), Mockito.any(ModuleExportStrategy.class)); + Assert.assertEquals(3, infos.size()); } @Test - public void testLoadPackagesWorksWithOnePackageInfo() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); - - Mockito.doCallRealMethod().when(packageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); - Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(0))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); - Mockito.doReturn(MockEntities.makeMockPackageInfo()).when(packageLoader).loadPackage(MOCK_FILES.get(0)); - - packageLoader.loadPackages(config); - - Mockito.verify(packageLoader).loadPackage(Mockito.any()); - } - - @Test(expected = NoModuleToInstrumentException.class) - public void testLoadPackagesFailsWithNoPackageInfoCreated() { + public void testLoadPackageWorks() throws Exception { JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); - Mockito.doCallRealMethod().when(packageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); - Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(0))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); + File file = JarFileTestUtils.createJar(temporaryFolder, "jarFile", "A.class", "B.class"); - packageLoader.loadPackages(config); + ModuleInfo info = packageLoader.loadPackage(file, null); - Mockito.verify(packageLoader).loadPackage(Mockito.any()); + Mockito.verify(packageLoader).injectFileToSystemClassPath(file); + Assert.assertEquals(2, info.getClassByteCodeMap().size()); + Assert.assertEquals(file, info.getFile()); + Assert.assertTrue(info.getClassByteCodeMap().containsKey("A")); + Assert.assertTrue(info.getClassByteCodeMap().containsKey("B")); + Assert.assertArrayEquals("A.class".getBytes(), info.getClassByteCodeMap().get("A")); + Assert.assertArrayEquals("B.class".getBytes(), info.getClassByteCodeMap().get("B")); } @Test - public void testLoadPackagesWorksAndCalledThreeTimesWithThreePaths() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); - - Mockito.doCallRealMethod().when(packageLoader).loadPackages(config); + public void testExtractEntriesWorks() { + JarFile jarFile = MockEntities.makeMockJarFile(); - Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(1))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); - Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(2))).thenReturn(Arrays.asList(MOCK_FILES.get(1))); - Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(3))).thenReturn(Arrays.asList(MOCK_FILES.get(2))); + List entries = loader.extractEntries(jarFile); - try { - packageLoader.loadPackages(config); - } catch (NoModuleToInstrumentException e) { - // swallow - } - - Mockito.verify(packageLoader, Mockito.times(3)).loadPackage(Mockito.any()); - } - - @Test - public void testLoadPackagesWorksAndReturnsValidPackageInfoObjectAndInvokesProcessFile() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(new JarModuleExportStrategy())); - - List classes = MOCK_JAR_ENTRIES - .stream() - .map(jarEntry -> jarEntry.getName().substring(0, jarEntry.getName().lastIndexOf(".class"))) - .collect(Collectors.toList()); - - Mockito.doCallRealMethod().when(packageLoader).loadPackage(MOCK_FILES.get(0)); - Mockito.doReturn(jarFile).when(packageLoader).processFile(MOCK_FILES.get(0)); - Mockito.doReturn(MOCK_JAR_ENTRIES).when(packageLoader).extractEntries(Mockito.any()); - - final ModuleInfo info = packageLoader.loadPackage(MOCK_FILES.get(0)); - - Mockito.verify(packageLoader, Mockito.times(1)).processFile(Mockito.any()); - Assert.assertTrue(info.getClassNames().size() == MOCK_JAR_ENTRIES.size()); - Assert.assertArrayEquals(classes.toArray(), info.getClassNames().toArray()); - Assert.assertSame(MOCK_FILES.get(0), info.getFile()); - Assert.assertSame(jarFile, info.getJarFile()); - Assert.assertSame(JarModuleExportStrategy.class, info.getExportStrategy().getClass()); + Assert.assertEquals(6, entries.size()); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarFileTestUtils.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarFileTestUtils.java new file mode 100644 index 0000000..3237c4c --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarFileTestUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.util; + +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +public class JarFileTestUtils { + public static File createJar(TemporaryFolder temporaryFolder, String fileName, String... entries) throws Exception { + File file = temporaryFolder.newFile(fileName + ".jar"); + try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + for (String entry : entries) { + //write a sentinal file with the same name as the jar, to test if it becomes readable by getResource. + jarOutputStream.putNextEntry(new ZipEntry(entry)); + jarOutputStream.write(entry.getBytes()); + jarOutputStream.closeEntry(); + } + } + } + return file; + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java index 18cca97..86e4148 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java @@ -62,6 +62,15 @@ public static List makeMockJarEntries() { return list; } + public static JarFile makeMockJarFile(){ + JarFile file = Mockito.mock(JarFile.class); + + Enumeration e = Collections.enumeration(makeMockJarEntriesWithPath()); + Mockito.when(file.entries()).thenReturn(e); + + return file; + } + public static Map makeInstrumentedClassesMap() { final Map classes = new HashMap<>(); final InstrumentedClassState stateOne = new InstrumentedClassState("installable_a", new byte[]{12}); @@ -74,13 +83,6 @@ public static Map makeInstrumentedClassesMap() { return classes; } - public static List makeMockFiles() { - return Arrays.asList(new File("file_a"), - new File("file_b"), - new File("file_c")); - } - - public static List makeMockPathsWithDuplicates() { return Arrays.asList("path_a", "path_a", "path_b", "path_c"); } @@ -113,22 +115,16 @@ public static DynamicType makeMockDynamicType(){ return type; } - public static ModuleInfo makeMockPackageInfo(){ + public static ModuleInfo makeMockModuleInfo(){ final ModuleInfo info = Mockito.mock(ModuleInfo.class); final File mockFile = Mockito.mock(File.class); - final JarFile mockJarFile = Mockito.mock(JarFile.class); final ModuleExportStrategy mockStrategy = Mockito.mock(ModuleExportStrategy.class); Mockito.lenient().when(info.getFile()).thenReturn(mockFile); - Mockito.lenient().when(info.getJarFile()).thenReturn(mockJarFile); - Mockito.lenient().when(mockJarFile.getName()).thenReturn("mock.jar"); Mockito.lenient().when(info.getExportStrategy()).thenReturn(mockStrategy); Mockito.lenient().when(mockFile.getName()).thenReturn("mock.jar"); - final Enumeration entries = Collections.enumeration(makeMockJarEntriesWithPath()); - Mockito.lenient().when(mockJarFile.entries()).thenReturn(entries); - return info; } } diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java index c9f128d..c85d8ce 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java @@ -1,6 +1,7 @@ package software.amazon.disco.agent.web.apache.event; import org.apache.http.ProtocolVersion; +import org.junit.After; import org.junit.Before; import org.junit.Test; import software.amazon.disco.agent.concurrent.TransactionContext; @@ -34,6 +35,13 @@ public void before() { TransactionContext.create(); EventBus.addListener(mockEventBusListener); } + + @After + public void after() { + EventBus.removeListener(mockEventBusListener); + TransactionContext.destroy(); + } + @Test public void testForRequestEventCreationForRequest() { HttpServiceDownstreamRequestEvent event = ApacheEventFactory.createDownstreamRequestEvent(ApacheTestConstants.APACHE_HTTP_CLIENT_ORIGIN, diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java index ff04d0b..bcd9847 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java @@ -80,7 +80,7 @@ public void before() { @After public void after() { - TransactionContext.clear(); + TransactionContext.destroy(); EventBus.removeListener(mockEventBusListener); } From a18ea0a2c4d7caac22749240272201081efd3e93 Mon Sep 17 00:00:00 2001 From: Hongbo Liu Date: Mon, 10 Aug 2020 10:37:05 -0400 Subject: [PATCH 36/45] refactored TransactionContext to not use lambda expression when initializing transactionContext field --- .../UnableToInstrumentException.java | 33 ---- .../export/JarModuleExportStrategy.java | 53 ++++-- .../export/ModuleExportStrategy.java | 5 +- .../instrumentation/ModuleTransformer.java | 57 +++---- .../TransformationListener.java | 21 +-- .../loaders/agents/DiscoAgentLoader.java | 2 + .../loaders/agents/TransformerExtractor.java | 102 ----------- .../loaders/modules/JarModuleLoader.java | 119 +++++++++---- .../loaders/modules/ModuleInfo.java | 4 +- .../preprocess/util/JarFileUtils.java | 57 ------- .../export/JarModuleExportStrategyTest.java | 26 ++- .../ModuleTransformerTest.java | 69 +++----- .../TransformationListenerTest.java | 6 - .../loaders/agents/DiscoAgentLoaderTest.java | 43 ++++- .../agents/TransformerExtractorTest.java | 42 ----- .../loaders/modules/JarModuleLoaderTest.java | 159 ++++++++++++++---- .../preprocess/util/JarFileTestUtils.java | 40 ----- .../preprocess/util/MockEntities.java | 24 +-- .../agent/concurrent/TransactionContext.java | 30 +++- 19 files changed, 392 insertions(+), 500 deletions(-) delete mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToInstrumentException.java delete mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java delete mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java delete mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java delete mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarFileTestUtils.java diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToInstrumentException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToInstrumentException.java deleted file mode 100644 index 8da2409..0000000 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToInstrumentException.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.instrumentation.preprocess.exceptions; - -import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; - -/** - * Exception thrown when the library fails to instrument a target class. - */ -public class UnableToInstrumentException extends RuntimeException { - /** - * Constructor accepting a message explaining the error as well as a {@link Throwable cause} - * - * @param message detailed message explaining the failure - * @param cause cause of the exception - */ - public UnableToInstrumentException(String message, Throwable cause) { - super(PreprocessConstants.MESSAGE_PREFIX + message, cause); - } -} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java index 564abbc..ebce64e 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java @@ -17,7 +17,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleExportException; import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; @@ -45,21 +44,41 @@ public class JarModuleExportStrategy implements ModuleExportStrategy { private static final Logger log = LogManager.getLogger(JarModuleExportStrategy.class); private static Path tempDir = null; + private final String outputDir; + + /** + * Constructor with the destination path provided. + * + * @param outputDir Absolute output path of the transformed jar. Default is the current folder where + * the original jar is located. + */ + public JarModuleExportStrategy(final String outputDir) { + this.outputDir = outputDir; + } + + /** + * Constructor with no destination path provided. Transformed jar will replace the original file. + */ + public JarModuleExportStrategy() { + this.outputDir = null; + } /** * Exports all transformed classes to a Jar file. A temporary Jar File will be created to store all * the transformed classes and then be renamed to replace the original Jar. * - * {@inheritDoc} + * @param moduleInfo Information of the original Jar + * @param instrumented a map of instrumented classes with their bytecode + * @param suffix suffix of the transformed package */ @Override - public void export(final ModuleInfo moduleInfo, final Map instrumented, final PreprocessConfig config) { + public void export(final ModuleInfo moduleInfo, final Map instrumented, final String suffix) { log.debug(PreprocessConstants.MESSAGE_PREFIX + "Saving changes to Jar"); final File file = createTempFile(moduleInfo); buildOutputJar(moduleInfo, instrumented, file); - moveTempFileToDestination(moduleInfo, config, file); + moveTempFileToDestination(moduleInfo, suffix, file); } /** @@ -111,11 +130,11 @@ protected void saveTransformedClasses(final JarOutputStream jarOS, final Map instrumented) { - for (Enumeration entries = jarFile.entries(); entries.hasMoreElements(); ) { + protected void copyExistingJarEntries(final JarOutputStream jarOS, final ModuleInfo moduleInfo, final Map instrumented) { + for (Enumeration entries = moduleInfo.getJarFile().entries(); entries.hasMoreElements(); ) { final JarEntry entry = (JarEntry) entries.nextElement(); final String keyToCheck = entry.getName().endsWith(".class") ? entry.getName().substring(0, entry.getName().lastIndexOf(".class")) : entry.getName(); @@ -124,7 +143,7 @@ protected void copyExistingJarEntries(final JarOutputStream jarOS, final JarFile if (entry.isDirectory()) { jarOS.putNextEntry(entry); } else { - copyJarEntry(jarOS, jarFile, entry); + copyJarEntry(jarOS, moduleInfo.getJarFile(), entry); } } } catch (IOException e) { @@ -141,8 +160,8 @@ protected void copyExistingJarEntries(final JarOutputStream jarOS, final JarFile * @param tempFile file that the JarOutputStream will write to */ protected void buildOutputJar(final ModuleInfo moduleInfo, final Map instrumented, final File tempFile) { - try (JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(tempFile)); JarFile jarFile = new JarFile(moduleInfo.getFile())) { - copyExistingJarEntries(jarOS, jarFile, instrumented); + try (JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(tempFile))) { + copyExistingJarEntries(jarOS, moduleInfo, instrumented); saveTransformedClasses(jarOS, instrumented); } catch (IOException e) { throw new ModuleExportException("Failed to create output Jar file", e); @@ -182,22 +201,22 @@ protected void copyJarEntry(final JarOutputStream jarOS, final JarFile file, fin /** * Move the temp file containing all existing entries and transformed classes to the - * destination. If {@link PreprocessConfig#getOutputDir()} is null, the original file will be replaced. + * destination. If {@link #outputDir} is NOT specified, the original file will be replaced. * * @param moduleInfo Information of the original Jar - * @param config configuration file containing instructions to instrument a module + * @param suffix suffix to be appended to the transformed package * @param tempFile output file to be moved to the destination path * @return {@link Path} of the overwritten file */ - protected Path moveTempFileToDestination(final ModuleInfo moduleInfo, final PreprocessConfig config, final File tempFile) { + protected Path moveTempFileToDestination(final ModuleInfo moduleInfo, final String suffix, final File tempFile) { try { - final String destinationStr = config.getOutputDir() == null ? + final String destinationStr = outputDir == null ? moduleInfo.getFile().getAbsolutePath() - : config.getOutputDir() + "/" + moduleInfo.getFile().getName(); + : outputDir + "/" + moduleInfo.getFile().getName(); - final String destinationStrWithSuffix = config.getSuffix() == null ? + final String destinationStrWithSuffix = suffix == null ? destinationStr - : destinationStr.substring(0, destinationStr.lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + config.getSuffix() + PreprocessConstants.JAR_EXTENSION; + : destinationStr.substring(0, destinationStr.lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + suffix + PreprocessConstants.JAR_EXTENSION; final Path destination = Paths.get(destinationStrWithSuffix); destination.toFile().getParentFile().mkdirs(); diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java index 833bae8..b2e0b78 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java @@ -15,7 +15,6 @@ package software.amazon.disco.instrumentation.preprocess.export; -import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; @@ -30,7 +29,7 @@ public interface ModuleExportStrategy { * * @param info Information of the original Jar * @param instrumented a map of instrumented classes with their bytecode - * @param config configuration file containing instructions to instrument a module + * @param suffix suffix to be appended to the transformed package */ - void export(final ModuleInfo info, final Map instrumented, final PreprocessConfig config); + void export(final ModuleInfo info, final Map instrumented, final String suffix); } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java index 19a128d..d2357df 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java @@ -21,20 +21,17 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator; +import software.amazon.disco.agent.inject.Injector; import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.AgentLoaderNotProvidedException; import software.amazon.disco.instrumentation.preprocess.exceptions.InvalidConfigEntryException; import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleLoaderNotProvidedException; -import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToInstrumentException; import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.loaders.agents.AgentLoader; -import software.amazon.disco.instrumentation.preprocess.loaders.agents.TransformerExtractor; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleLoader; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; -import java.lang.instrument.ClassFileTransformer; -import java.lang.instrument.IllegalClassFormatException; import java.util.Map; /** @@ -59,30 +56,29 @@ public class ModuleTransformer { * and trigger the program to exit with status 1 */ public void transform() { - if (config == null || config.getLogLevel() == null) { - Configurator.setRootLevel(Level.INFO); - } else { - Configurator.setRootLevel(config.getLogLevel()); - } - try { - if (config == null) { - throw new InvalidConfigEntryException("No configuration provided", null); - } - if (agentLoader == null) { - throw new AgentLoaderNotProvidedException(); + if (config == null) {throw new InvalidConfigEntryException("No configuration provided", null);} + + if (config.getLogLevel() == null) { + Configurator.setRootLevel(Level.INFO); + } else { + Configurator.setRootLevel(config.getLogLevel()); } + + if (agentLoader == null) {throw new AgentLoaderNotProvidedException();} + if (jarLoader == null) {throw new ModuleLoaderNotProvidedException();} + + agentLoader.loadAgent(config, Injector.createInstrumentation()); + if (jarLoader == null) { throw new ModuleLoaderNotProvidedException(); } - agentLoader.loadAgent(config, new TransformerExtractor()); - - // Apply instrumentation on all loaded jars + // Apply instrumentation on all jars for (final ModuleInfo info : jarLoader.loadPackages(config)) { applyInstrumentation(info); + //todo: store serialized instrumentation state to target jar } - } catch (RuntimeException e) { log.error(e); System.exit(1); @@ -90,25 +86,22 @@ public void transform() { } /** - * Triggers instrumentation of classes using Reflection and applies and saves the changes according to the provided - * {@link ModuleExportStrategy export strategy} on a local file + * Triggers instrumentation of classes using Reflection and applies the changes according to the + * {@link ModuleExportStrategy export strategy} + * of this package * - * @param moduleInfo meta-data of a loaded jar file + * @param moduleInfo a package containing classes to be instrumented */ - protected void applyInstrumentation(ModuleInfo moduleInfo) { - log.info("applying transformation on: " + moduleInfo.getFile().getAbsolutePath()); - for (Map.Entry entry : moduleInfo.getClassByteCodeMap().entrySet()) { + protected void applyInstrumentation(final ModuleInfo moduleInfo) { + for (String name : moduleInfo.getClassNames()) { try { - for (ClassFileTransformer transformer : TransformerExtractor.getTransformers()) { - transformer.transform(getClass().getClassLoader(), entry.getKey(), null, null, entry.getValue()); - } - } catch (IllegalClassFormatException e) { - throw new UnableToInstrumentException("Failed to instrument : " + entry.getKey(), e); + Class.forName(name); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + log.warn(PreprocessConstants.MESSAGE_PREFIX + "Failed to initialize class:" + name, e); } } - log.debug(PreprocessConstants.MESSAGE_PREFIX + getInstrumentedClasses().size() + " classes transformed"); - moduleInfo.getExportStrategy().export(moduleInfo, getInstrumentedClasses(), config); + moduleInfo.getExportStrategy().export(moduleInfo, getInstrumentedClasses(), config.getSuffix()); // empty the map in preparation for transforming another package getInstrumentedClasses().clear(); diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java index ed5098a..1f83a5a 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java @@ -1,18 +1,3 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - package software.amazon.disco.instrumentation.preprocess.instrumentation; import lombok.Getter; @@ -22,7 +7,7 @@ import net.bytebuddy.utility.JavaModule; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToInstrumentException; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; import java.util.HashMap; import java.util.Map; @@ -73,7 +58,7 @@ public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, * {@inheritDoc} */ public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) { - throw new UnableToInstrumentException("Failed to instrument : " + typeName, throwable); + log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to instrument: " + typeName, throwable); } /** @@ -97,7 +82,7 @@ protected void collectDataFromEvent(TypeDescription typeDescription, DynamicType } if (!dynamicType.getAuxiliaryTypes().isEmpty()) { - for (Map.Entry auxiliaryEntry : dynamicType.getAuxiliaryTypes().entrySet()) { + for(Map.Entry auxiliaryEntry : dynamicType.getAuxiliaryTypes().entrySet()){ instrumentedTypes.put(auxiliaryEntry.getKey().getInternalName(), new InstrumentedClassState(null, auxiliaryEntry.getValue())); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java index d2cb083..fa43aa0 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java @@ -51,6 +51,8 @@ public void loadAgent(final PreprocessConfig config, Instrumentation instrumenta throw new NoAgentToLoadException(); } + instrumentation = instrumentation == null ? Injector.createInstrumentation() : instrumentation; + final ClassFileVersion version = parseClassFileVersionFromConfig(config); DiscoAgentTemplate.setAgentConfigFactory(() -> { diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java deleted file mode 100644 index 7913f34..0000000 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.instrumentation.preprocess.loaders.agents; - -import lombok.Getter; -import software.amazon.disco.agent.inject.Injector; - -import java.lang.instrument.ClassDefinition; -import java.lang.instrument.ClassFileTransformer; -import java.lang.instrument.Instrumentation; -import java.util.ArrayList; -import java.util.List; -import java.util.jar.JarFile; - -/** - * A No-op implementation of the {@link Instrumentation} interface that extracts all {@link ClassFileTransformer transformers} installed - * onto the instance and appends the agent jar to the bootstrap class path. - */ -public class TransformerExtractor implements Instrumentation { - @Getter - private static List transformers = new ArrayList<>(); - - @Override - public void addTransformer(ClassFileTransformer transformer, boolean canRetransform) { - transformers.add(transformer); - } - - @Override - public void addTransformer(ClassFileTransformer transformer) { - transformers.add(transformer); - } - - @Override - public boolean removeTransformer(ClassFileTransformer transformer) { - return false; - } - - @Override - public boolean isRetransformClassesSupported() { - return true; - } - - @Override - public void retransformClasses(Class... classes) { } - - @Override - public boolean isRedefineClassesSupported() { - return true; - } - - @Override - public void redefineClasses(ClassDefinition... definitions) { } - - @Override - public boolean isModifiableClass(Class theClass) { - return false; - } - - @Override - public Class[] getAllLoadedClasses() { - return new Class[0]; - } - - @Override - public Class[] getInitiatedClasses(ClassLoader loader) { - return new Class[0]; - } - - @Override - public long getObjectSize(Object objectToSize) { - return 0; - } - - @Override - public void appendToBootstrapClassLoaderSearch(JarFile jarfile) { - Injector.createInstrumentation().appendToBootstrapClassLoaderSearch(jarfile); - } - - @Override - public void appendToSystemClassLoaderSearch(JarFile jarfile) { } - - @Override - public boolean isNativeMethodPrefixSupported() { - return false; - } - - @Override - public void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix) { } -} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java index ee5e964..9cd3368 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java @@ -15,23 +15,22 @@ package software.amazon.disco.instrumentation.preprocess.loaders.modules; +import lombok.Getter; +import software.amazon.disco.agent.inject.Injector; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import software.amazon.disco.agent.inject.Injector; import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; -import software.amazon.disco.instrumentation.preprocess.util.JarFileUtils; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Enumeration; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -41,13 +40,30 @@ public class JarModuleLoader implements ModuleLoader { private static final Logger log = LogManager.getLogger(JarModuleLoader.class); + @Getter + private final ModuleExportStrategy strategy; + + /** + * Default constructor that sets {@link #strategy} to {@link JarModuleExportStrategy} + */ + public JarModuleLoader() { + this.strategy = new JarModuleExportStrategy(); + } + + /** + * Constructor accepting a custom export strategy + * + * @param strategy {@link ModuleExportStrategy strategy} for exporting transformed classes under this path. Default strategy is {@link JarModuleExportStrategy} + */ + public JarModuleLoader(final ModuleExportStrategy strategy) { + this.strategy = strategy; + } + /** * {@inheritDoc} */ @Override public List loadPackages(PreprocessConfig config) { - log.info(PreprocessConstants.MESSAGE_PREFIX + "Loading packages"); - if (config == null || config.getJarPaths() == null) { throw new NoModuleToInstrumentException(); } @@ -55,13 +71,14 @@ public List loadPackages(PreprocessConfig config) { final List packageEntries = new ArrayList<>(); for (String path : config.getJarPaths()) { - final ModuleInfo info = loadPackage(new File(path), new JarModuleExportStrategy()); + for (File file : discoverFilesInPath(path)) { + final ModuleInfo info = loadPackage(file); - if (info != null) { - packageEntries.add(info); + if (info != null) { + packageEntries.add(info); + } } } - if (packageEntries.isEmpty()) { throw new NoModuleToInstrumentException(); } @@ -69,39 +86,43 @@ public List loadPackages(PreprocessConfig config) { } /** - * Helper method to load one single Jar file + * Discovers all files under a path * - * @param file Jar file to be loaded - * @return {@link ModuleInfo object} containing package data, null if file is not a valid {@link JarFile} + * @return a List of {@link File files}, empty is no files found */ - protected ModuleInfo loadPackage(final File file, final ModuleExportStrategy strategy) { - log.info(PreprocessConstants.MESSAGE_PREFIX + "Loading module: " + file.getAbsolutePath()); + protected List discoverFilesInPath(final String path) { + final List files = new ArrayList<>(); + final File packageDir = new File(path); - try (JarFile jarFile = new JarFile(file)) { - final Map types = new HashMap<>(); + final File[] packageFiles = packageDir.listFiles(); - if (jarFile == null) { - log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to load module: "+file.getAbsolutePath()); - return null; - } + if (packageFiles == null) { + log.debug(PreprocessConstants.MESSAGE_PREFIX + "No packages found under path: " + path); + return files; + } - injectFileToSystemClassPath(file); + files.addAll(Arrays.asList(packageFiles)); + return files; + } - log.info(PreprocessConstants.MESSAGE_PREFIX + "Module loaded"); + /** + * Helper method to load one single Jar file + * + * @param file Jar file to be loaded + * @return {@link ModuleInfo object} containing package data, null if file is not a valid {@link JarFile} + */ + protected ModuleInfo loadPackage(final File file) { + final JarFile jarFile = processFile(file); - for (JarEntry entry : extractEntries(jarFile)) { - if (entry.getName().endsWith(".class")) { - final String nameWithoutExtension = entry.getName().substring(0, entry.getName().lastIndexOf(".class")).replace('/', '.'); + if (jarFile == null) return null; - types.put(nameWithoutExtension, JarFileUtils.readEntryFromJar(jarFile, entry)); - } - } + final List names = new ArrayList<>(); - return types.isEmpty() ? null : new ModuleInfo(file, strategy, types); - } catch (IOException e) { - log.error(PreprocessConstants.MESSAGE_PREFIX + "Invalid file, skipped", e); - return null; + for (JarEntry entry : extractEntries(jarFile)) { + names.add(entry.getName().substring(0, entry.getName().lastIndexOf(".class")).replace('/', '.')); } + + return names.isEmpty() ? null : new ModuleInfo(file, jarFile, names, strategy); } /** @@ -127,6 +148,38 @@ protected List extractEntries(final JarFile jarFile) { return result; } + /** + * Validates the file and adds it to the system class path + * + * @param file file to process + * @return a valid {@link JarFile}, null if Jar cannot be created from {@link File} passed in. + */ + protected JarFile processFile(final File file) { + if (file.isDirectory() || !file.getName().toLowerCase().contains(PreprocessConstants.JAR_EXTENSION)) return null; + + final JarFile jar = makeJarFile(file); + + if (jar != null) { + injectFileToSystemClassPath(file); + } + return jar; + } + + /** + * Creates a {@link JarFile} from {@link File}. + * + * @param file file to construct the Jar file from + * @return a valid {@link JarFile}, null if invalid + */ + protected JarFile makeJarFile(final File file) { + try { + return new JarFile(file); + } catch (IOException e) { + log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to create JarFile from file: " + file.getName(), e); + return null; + } + } + /** * Add the file to the system class path using the {@link Injector injector} api. * diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java index fd54f1e..999e9c4 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java @@ -21,7 +21,6 @@ import java.io.File; import java.util.List; -import java.util.Map; import java.util.jar.JarFile; /** @@ -32,6 +31,7 @@ @Getter public class ModuleInfo { private final File file; + private final JarFile jarFile; + private final List classNames; private final ModuleExportStrategy exportStrategy; - private final Map classByteCodeMap; } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java deleted file mode 100644 index 1e987f6..0000000 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.instrumentation.preprocess.util; - -import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -/** - * Utility class for performing JarFile related tasks. - */ -public class JarFileUtils { - - /** - * Reads the byte[] of a JarEntry from a JarFile - * - * @param jarfile JarFile where the binary data will be read - * @param entry JarEntry to be read - * @return byte[] of the entry - * @throws UnableToReadJarEntryException - */ - public static byte[] readEntryFromJar(JarFile jarfile, JarEntry entry) { - try (final InputStream entryStream = jarfile.getInputStream(entry)) { - if (entryStream == null) { - throw new UnableToReadJarEntryException(entry.getName(), null); - } - - final byte[] buffer = new byte[2048]; - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - for (int len = entryStream.read(buffer); len != -1; len = entryStream.read(buffer)) { - os.write(buffer, 0, len); - } - return os.toByteArray(); - - } catch (IOException e) { - throw new UnableToReadJarEntryException(entry.getName(), null); - } - } -} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java index 78d500b..b6c2485 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java @@ -22,7 +22,7 @@ import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; -import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; +import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleExportException; import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; @@ -54,7 +54,6 @@ public class JarModuleExportStrategyTest { JarOutputStream mockJarOS; JarModuleExportStrategy mockStrategy; JarModuleExportStrategy spyStrategy; - PreprocessConfig config; @Before public void before() throws IOException { @@ -64,9 +63,7 @@ public void before() throws IOException { mockJarOS = Mockito.mock(JarOutputStream.class); spyStrategy = Mockito.spy(new JarModuleExportStrategy()); - mockModuleInfo = MockEntities.makeMockModuleInfo(); - config = PreprocessConfig.builder().build(); - + mockModuleInfo = MockEntities.makeMockPackageInfo(); Mockito.doCallRealMethod().when(mockStrategy).export(Mockito.any(), Mockito.any(), Mockito.any()); Mockito.when(mockStrategy.createTempFile(Mockito.any())).thenReturn(tempFolder.newFile(TEMP_FILE_NAME)); } @@ -106,10 +103,9 @@ public void testSaveTransformedClassesWorksAndCreatesNewEntries() throws IOExcep @Test public void testCopyExistingJarEntriesWorksWithFilesAndPath() throws IOException { - JarFile jarFile = MockEntities.makeMockJarFile(); Mockito.doCallRealMethod().when(mockStrategy).copyExistingJarEntries(Mockito.eq(mockJarOS), Mockito.any(), Mockito.any()); - mockStrategy.copyExistingJarEntries(mockJarOS, jarFile, MockEntities.makeInstrumentedClassesMap()); + mockStrategy.copyExistingJarEntries(mockJarOS, mockModuleInfo, MockEntities.makeInstrumentedClassesMap()); // 3 out of 6 classes have not been instrumented Mockito.verify(mockStrategy, Mockito.times(3)).copyJarEntry(Mockito.eq(mockJarOS), Mockito.any(), Mockito.any()); @@ -154,7 +150,7 @@ public void testMoveTempFileToDestinationToReplaceOriginal() throws IOException File file = spyStrategy.createTempFile(mockModuleInfo); // replace original file - Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, config, file); + Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, null, file); Assert.assertNotEquals(originalLength, path.toFile().length()); Assert.assertEquals(originalFile.getAbsolutePath(), path.toFile().getAbsolutePath()); @@ -165,8 +161,7 @@ public void testMoveTempFileToDestinationToReplaceOriginal() throws IOException @Test public void testMoveTempFileToDestinationWorks() throws IOException { File outDir = tempFolder.newFolder(OUT_DIR); - config = PreprocessConfig.builder().outputDir(outDir.getAbsolutePath()).build(); - JarModuleExportStrategy spyStrategy = new JarModuleExportStrategy(); + spyStrategy = new JarModuleExportStrategy(outDir.getAbsolutePath()); // create original file and assume temp/disco/tests is where the original package is File originalFile = createOriginalFile(); @@ -177,7 +172,7 @@ public void testMoveTempFileToDestinationWorks() throws IOException { File file = spyStrategy.createTempFile(mockModuleInfo); // move to destination - Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, config, file); + Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, null, file); Assert.assertEquals(outDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); Assert.assertEquals(mockModuleInfo.getFile().getName(), path.toFile().getName()); @@ -188,9 +183,8 @@ public void testMoveTempFileToDestinationWorks() throws IOException { @Test public void testMoveTempFileToDestinationWorksWithSuffix() throws IOException { - File outDir = tempFolder.newFolder(OUT_DIR); - config = PreprocessConfig.builder().suffix(PACKAGE_SUFFIX).outputDir(outDir.getAbsolutePath()).build(); - spyStrategy = new JarModuleExportStrategy(); + File outputDir = tempFolder.newFolder(OUT_DIR); + spyStrategy = new JarModuleExportStrategy(outputDir.getAbsolutePath()); // create original file and assume temp/disco/tests is where the original package is File originalFile = createOriginalFile(); @@ -199,7 +193,7 @@ public void testMoveTempFileToDestinationWorksWithSuffix() throws IOException { // move to destination Mockito.when(mockModuleInfo.getFile()).thenReturn(originalFile); - Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, config, tempFile); + Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, PACKAGE_SUFFIX, tempFile); String nameToCheck = mockModuleInfo.getFile() .getName() @@ -207,7 +201,7 @@ public void testMoveTempFileToDestinationWorksWithSuffix() throws IOException { + PACKAGE_SUFFIX + PreprocessConstants.JAR_EXTENSION; - Assert.assertEquals(outDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); + Assert.assertEquals(outputDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); Assert.assertEquals(nameToCheck, path.toFile().getName()); Assert.assertTrue(originalFile.exists()); } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java index e240a53..a54ac0f 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java @@ -17,7 +17,6 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -28,16 +27,14 @@ import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.loaders.agents.DiscoAgentLoader; -import software.amazon.disco.instrumentation.preprocess.loaders.agents.TransformerExtractor; import software.amazon.disco.instrumentation.preprocess.loaders.modules.JarModuleLoader; import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; import software.amazon.disco.instrumentation.preprocess.util.MockEntities; -import java.io.File; -import java.lang.instrument.ClassFileTransformer; -import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.List; import java.util.Map; @RunWith(MockitoJUnitRunner.class) @@ -45,7 +42,6 @@ public class ModuleTransformerTest { private static final String PACKAGE_SUFFIX = "suffix"; ModuleTransformer spyTransformer; - PreprocessConfig config; @Mock DiscoAgentLoader mockAgentLoader; @@ -53,8 +49,8 @@ public class ModuleTransformerTest { @Mock JarModuleLoader mockJarPackageLoader; - @Mock - ModuleInfo moduleInfo; + PreprocessConfig config; + List moduleInfos; @Before public void before() { @@ -72,13 +68,19 @@ public void before() { .build() ); - Mockito.doReturn(Arrays.asList(MockEntities.makeMockModuleInfo())) + Mockito.doReturn(Arrays.asList(MockEntities.makeMockPackageInfo())) .when(mockJarPackageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); + + moduleInfos = new ArrayList<>(); + moduleInfos.add(Mockito.mock(ModuleInfo.class)); + moduleInfos.add(Mockito.mock(ModuleInfo.class)); } - @After - public void after(){ - TransformerExtractor.getTransformers().clear(); + @Test + public void testTransformWorksWithDefaultLogLevel() { + spyTransformer.transform(); + + Assert.assertEquals(LogManager.getLogger().getLevel(), Level.INFO); } @Test @@ -98,12 +100,6 @@ public void testTransformWorksWithVerboseLogLevel() { Assert.assertEquals(Level.TRACE, LogManager.getLogger().getLevel()); } - @Test - public void testTransformWorksWithDefaultLogLevel() { - spyTransformer.transform(); - Assert.assertEquals(LogManager.getLogger().getLevel(), Level.INFO); - } - @Test public void testTransformWorksAndInvokesLoadAgentAndPackages() { spyTransformer = Mockito.spy( @@ -115,7 +111,7 @@ public void testTransformWorksAndInvokesLoadAgentAndPackages() { ); spyTransformer.transform(); - Mockito.verify(mockAgentLoader).loadAgent(Mockito.any(PreprocessConfig.class), Mockito.any(TransformerExtractor.class)); + Mockito.verify(mockAgentLoader).loadAgent(Mockito.any(PreprocessConfig.class), Mockito.any(Instrumentation.class)); Mockito.verify(mockJarPackageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); } @@ -127,34 +123,21 @@ public void testTransformWorksAndInvokesPackageLoader() { Mockito.verify(spyTransformer).applyInstrumentation(Mockito.any()); } + @Test - public void testApplyInstrumentationWorks() throws IllegalClassFormatException { - JarModuleExportStrategy strategy = Mockito.mock(JarModuleExportStrategy.class); + public void testApplyInstrumentationWorksAndInvokesExport() { + Mockito.doCallRealMethod().when(spyTransformer).applyInstrumentation(Mockito.any()); + + JarModuleExportStrategy s1 = Mockito.mock(JarModuleExportStrategy.class); + Mockito.when(moduleInfos.get(0).getExportStrategy()).thenReturn(s1); + Map instrumentedClasses = MockEntities.makeInstrumentedClassesMap(); - File file = Mockito.mock(File.class); - - Map byteArrayMap = new HashMap<>(); - byteArrayMap.put("ClassA", new byte[]{1}); - byteArrayMap.put("ClassB", new byte[]{2}); - - TransformerExtractor transformerExtractor = new TransformerExtractor(); - ClassFileTransformer transformer_1 = Mockito.mock(ClassFileTransformer.class); - ClassFileTransformer transformer_2 = Mockito.mock(ClassFileTransformer.class); - transformerExtractor.addTransformer(transformer_1); - transformerExtractor.addTransformer(transformer_2); - - Mockito.when(moduleInfo.getExportStrategy()).thenReturn(strategy); - Mockito.when(moduleInfo.getClassByteCodeMap()).thenReturn(byteArrayMap); - Mockito.when(moduleInfo.getFile()).thenReturn(file); - Mockito.when(file.getAbsolutePath()).thenReturn("mock/path"); Mockito.doReturn(instrumentedClasses).when(spyTransformer).getInstrumentedClasses(); - spyTransformer.applyInstrumentation(moduleInfo); + spyTransformer.applyInstrumentation(moduleInfos.get(0)); - Mockito.verify(moduleInfo).getClassByteCodeMap(); - Mockito.verify(strategy).export(moduleInfo, instrumentedClasses, config); - Mockito.verify(transformer_1).transform(Mockito.any(ClassLoader.class), Mockito.eq("ClassA"), Mockito.eq(null), Mockito.eq(null), Mockito.eq(new byte[]{1})); - Mockito.verify(transformer_1).transform(Mockito.any(ClassLoader.class), Mockito.eq("ClassB"), Mockito.eq(null), Mockito.eq(null), Mockito.eq(new byte[]{2})); + Mockito.verify(moduleInfos.get(0)).getClassNames(); + Mockito.verify(s1).export(moduleInfos.get(0), instrumentedClasses, PACKAGE_SUFFIX); Assert.assertTrue(instrumentedClasses.isEmpty()); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java index 159286a..b05462e 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java @@ -2,7 +2,6 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.DynamicType; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -29,11 +28,6 @@ public void before() { Mockito.when(mockTypeDescription.getInternalName()).thenReturn(MockEntities.makeClassPaths().get(0)); } - @After - public void after() { - TransformationListener.getInstrumentedTypes().clear(); - } - @Test public void testOnTransformationWorksAndInvokesCollectDataFromEvent() { Mockito.doCallRealMethod().when(mockListener).onTransformation(mockTypeDescription, null, null, false, mockDynamicTypeWithAuxiliary); diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java index 4227ff4..4aa5cb1 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java @@ -30,12 +30,14 @@ import software.amazon.disco.instrumentation.preprocess.exceptions.InvalidConfigEntryException; import software.amazon.disco.instrumentation.preprocess.exceptions.NoAgentToLoadException; import software.amazon.disco.instrumentation.preprocess.instrumentation.TransformationListener; -import software.amazon.disco.instrumentation.preprocess.util.JarFileTestUtils; import java.io.File; +import java.io.FileOutputStream; import java.lang.instrument.Instrumentation; import java.util.function.Supplier; import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; public class DiscoAgentLoaderTest { @Rule @@ -47,7 +49,7 @@ public void testLoadAgentFailOnNullPaths() throws NoAgentToLoadException { } @Test - public void testParsingJavaVersionWorks() { + public void testParsingJavaVersionWorks(){ PreprocessConfig config = PreprocessConfig.builder() .agentPath("path") .javaVersion("11") @@ -64,7 +66,7 @@ public void testParsingJavaVersionWorks() { } @Test(expected = InvalidConfigEntryException.class) - public void testParsingJavaVersionFailsWithInvalidJavaVersion() { + public void testParsingJavaVersionFailsWithInvalidJavaVersion(){ PreprocessConfig config = PreprocessConfig.builder() .agentPath("path") .javaVersion("a version") @@ -74,11 +76,11 @@ public void testParsingJavaVersionFailsWithInvalidJavaVersion() { @Test public void testLoadAgentRegistersAgentBuilderTransformerAndInstallsAgent() throws Exception { - Instrumentation instrumentation = Mockito.spy(new TransformerExtractor()); + Instrumentation instrumentation = Mockito.mock(Instrumentation.class); AgentBuilder agentBuilder = Mockito.mock(AgentBuilder.class); Mockito.when(agentBuilder.with(Mockito.any(ByteBuddy.class))).thenReturn(agentBuilder); - File file = JarFileTestUtils.createJar(temporaryFolder, "TestJarFile", "Foo.class"); + File file = createJar("TestJarFile"); PreprocessConfig config = PreprocessConfig.builder().agentPath(file.getAbsolutePath()).build(); Assert.assertNull(DiscoAgentTemplate.getAgentConfigFactory()); @@ -93,7 +95,8 @@ public void testLoadAgentRegistersAgentBuilderTransformerAndInstallsAgent() thro agentConfigSupplier.get().getAgentBuilderTransformer().apply(agentBuilder, null); Mockito.verify(agentBuilder).with(Mockito.any(TransformationListener.class)); - // check if a ByteBuddy instance with the correct java version is being installed using its own equals method + // check if a ByteBuddy instance with the correct java version is being installed using its own + // equals method Assert.assertEquals(ClassFileVersion.JAVA_V8, DiscoAgentLoader.parseClassFileVersionFromConfig(config)); ArgumentCaptor byteBuddyArgumentCaptor = ArgumentCaptor.forClass(ByteBuddy.class); Mockito.verify(agentBuilder).with(byteBuddyArgumentCaptor.capture()); @@ -104,4 +107,32 @@ public void testLoadAgentRegistersAgentBuilderTransformerAndInstallsAgent() thro Mockito.verify(instrumentation).appendToBootstrapClassLoaderSearch(jarFileArgumentCaptor.capture()); Assert.assertEquals(file.getAbsolutePath(), jarFileArgumentCaptor.getValue().getName()); } + + private File createJar(String name) throws Exception { + File file = temporaryFolder.newFile(name+".jar"); + try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + //write a sentinal file with the same name as the jar, to test if it becomes readable by getResource. + jarOutputStream.putNextEntry(new ZipEntry(name)); + jarOutputStream.write("foobar".getBytes()); + jarOutputStream.closeEntry(); + } + } + return file; + } + +// class MockAgentBuilderTransformer implements BiFunction { +// @Override +// public AgentBuilder apply(AgentBuilder agentBuilder, Installable installable) { +// return agentBuilder +// .with(new ByteBuddy(version)) +// .with(new TransformationListener(uuidGenerate(installable))); +// } +// +// class ByteBuddyTest extends ByteBuddy{ +// public ClassFileVersion getClassFileVersion(){ +// return classFileVersion; +// } +// } +// } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java deleted file mode 100644 index 4425e49..0000000 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.instrumentation.preprocess.loaders.agents; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; - -import java.lang.instrument.ClassFileTransformer; - -public class TransformerExtractorTest { - @After - public void after(){ - TransformerExtractor.getTransformers().clear(); - } - - @Test - public void testAddTransformerWorks(){ - ClassFileTransformer classFileTransformer = Mockito.mock(ClassFileTransformer.class); - - TransformerExtractor extractor = new TransformerExtractor(); - extractor.addTransformer(classFileTransformer); - extractor.addTransformer(classFileTransformer); - extractor.addTransformer(classFileTransformer); - - Assert.assertEquals(3, TransformerExtractor.getTransformers().size()); - } -} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java index 1a1c5e4..2d660d0 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java @@ -17,86 +17,177 @@ import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; +import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; -import software.amazon.disco.instrumentation.preprocess.util.JarFileTestUtils; import software.amazon.disco.instrumentation.preprocess.util.MockEntities; import java.io.File; +import java.util.Arrays; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.stream.Collectors; +@RunWith(MockitoJUnitRunner.class) public class JarModuleLoaderTest { + static final List PATHS = MockEntities.makeMockPathsWithDuplicates(); + static final List MOCK_FILES = MockEntities.makeMockFiles(); + static final List MOCK_JAR_ENTRIES = MockEntities.makeMockJarEntries(); + JarModuleLoader loader; PreprocessConfig config; - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Mock + JarFile jarFile; + + @Mock + File mockFile; @Before - public void before() { - config = PreprocessConfig.builder().jarPaths(MockEntities.makeMockPathsWithDuplicates()).build(); + public void before(){ + config = PreprocessConfig.builder().jarPaths(PATHS).build(); loader = new JarModuleLoader(); + + Mockito.when(mockFile.isDirectory()).thenReturn(false); + Mockito.when(mockFile.getName()).thenReturn("ATestJar.jar"); } @Test(expected = NoModuleToInstrumentException.class) - public void testLoadPackagesFailWithEmptyPathList() { - loader.loadPackages(config); + public void testConstructorFailWithEmptyPathList() { + new JarModuleLoader().loadPackages(config); } @Test(expected = NoModuleToInstrumentException.class) - public void testLoadPackagesFailWithNullConfig() { - loader.loadPackages(null); + public void testConstructorFailWithNullConfig() { + new JarModuleLoader().loadPackages(null); } @Test(expected = NoModuleToInstrumentException.class) - public void testLoadPackagesFailWithNullPathList() { - loader.loadPackages(PreprocessConfig.builder().build()); + public void testConstructorFailWithNullPathList() { + new JarModuleLoader().loadPackages(PreprocessConfig.builder().build()); + } + + @Test + public void testConstructorWorksAndHasDefaultStrategy() { + Assert.assertTrue(loader.getStrategy().getClass().equals(JarModuleExportStrategy.class)); + } + + @Test + public void testConstructorWorksWithNonDefaultStrategy() { + ModuleExportStrategy mockStrategy = Mockito.mock(ModuleExportStrategy.class); + + loader = new JarModuleLoader(mockStrategy); + Assert.assertNotEquals(JarModuleExportStrategy.class, loader.getStrategy().getClass()); + } + + @Test + public void testProcessFileWorksWithValidFileExtension(){ + JarModuleLoader loader = Mockito.mock(JarModuleLoader.class); + JarFile jar = Mockito.mock(JarFile.class); + + Mockito.doCallRealMethod().when(loader).processFile(mockFile); + Mockito.doReturn(jar).when(loader).makeJarFile(mockFile); + + Assert.assertNotNull(loader.processFile(mockFile)); + + Mockito.when(mockFile.getName()).thenReturn("ATestJar.JAR"); + Assert.assertNotNull(loader.processFile(mockFile)); + } + + @Test + public void testProcessFileWorksWithInvalidFileExtensionAndReturnNull(){ + JarModuleLoader loader = Mockito.mock(JarModuleLoader.class); + + Mockito.when(mockFile.getName()).thenReturn("ATestJar.txt"); + Mockito.doCallRealMethod().when(loader).processFile(mockFile); + + Assert.assertNull(loader.processFile(mockFile)); } @Test - public void testLoadPackagesWorksWithMultiplePaths() { + public void testProcessFileWorksAndInvokesInjectFileToSystemClassPath() { JarModuleLoader packageLoader = Mockito.mock(JarModuleLoader.class); - ModuleInfo info = Mockito.mock(ModuleInfo.class); + Mockito.when(packageLoader.processFile(Mockito.any())).thenCallRealMethod(); - Mockito.doCallRealMethod().when(packageLoader).loadPackages(config); - Mockito.doReturn(info).when(packageLoader).loadPackage(Mockito.any(File.class), Mockito.any(ModuleExportStrategy.class)); + JarFile mockJarfile = Mockito.mock(JarFile.class); + Mockito.when(packageLoader.makeJarFile(mockFile)).thenReturn(mockJarfile); - List infos = packageLoader.loadPackages(config); + packageLoader.processFile(mockFile); - Mockito.verify(packageLoader, Mockito.times(3)).loadPackage(Mockito.any(File.class), Mockito.any(ModuleExportStrategy.class)); - Assert.assertEquals(3, infos.size()); + Mockito.verify(packageLoader).injectFileToSystemClassPath(mockFile); } @Test - public void testLoadPackageWorks() throws Exception { + public void testLoadPackagesWorksWithOnePackageInfo() { + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); + + Mockito.doCallRealMethod().when(packageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); + Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(0))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); + Mockito.doReturn(MockEntities.makeMockPackageInfo()).when(packageLoader).loadPackage(MOCK_FILES.get(0)); + + packageLoader.loadPackages(config); + + Mockito.verify(packageLoader).loadPackage(Mockito.any()); + } + + @Test(expected = NoModuleToInstrumentException.class) + public void testLoadPackagesFailsWithNoPackageInfoCreated() { JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); - File file = JarFileTestUtils.createJar(temporaryFolder, "jarFile", "A.class", "B.class"); + Mockito.doCallRealMethod().when(packageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); + Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(0))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); - ModuleInfo info = packageLoader.loadPackage(file, null); + packageLoader.loadPackages(config); - Mockito.verify(packageLoader).injectFileToSystemClassPath(file); - Assert.assertEquals(2, info.getClassByteCodeMap().size()); - Assert.assertEquals(file, info.getFile()); - Assert.assertTrue(info.getClassByteCodeMap().containsKey("A")); - Assert.assertTrue(info.getClassByteCodeMap().containsKey("B")); - Assert.assertArrayEquals("A.class".getBytes(), info.getClassByteCodeMap().get("A")); - Assert.assertArrayEquals("B.class".getBytes(), info.getClassByteCodeMap().get("B")); + Mockito.verify(packageLoader).loadPackage(Mockito.any()); } @Test - public void testExtractEntriesWorks() { - JarFile jarFile = MockEntities.makeMockJarFile(); + public void testLoadPackagesWorksAndCalledThreeTimesWithThreePaths() { + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); + + Mockito.doCallRealMethod().when(packageLoader).loadPackages(config); - List entries = loader.extractEntries(jarFile); + Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(1))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); + Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(2))).thenReturn(Arrays.asList(MOCK_FILES.get(1))); + Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(3))).thenReturn(Arrays.asList(MOCK_FILES.get(2))); - Assert.assertEquals(6, entries.size()); + try { + packageLoader.loadPackages(config); + } catch (NoModuleToInstrumentException e) { + // swallow + } + + Mockito.verify(packageLoader, Mockito.times(3)).loadPackage(Mockito.any()); + } + + @Test + public void testLoadPackagesWorksAndReturnsValidPackageInfoObjectAndInvokesProcessFile() { + JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(new JarModuleExportStrategy())); + + List classes = MOCK_JAR_ENTRIES + .stream() + .map(jarEntry -> jarEntry.getName().substring(0, jarEntry.getName().lastIndexOf(".class"))) + .collect(Collectors.toList()); + + Mockito.doCallRealMethod().when(packageLoader).loadPackage(MOCK_FILES.get(0)); + Mockito.doReturn(jarFile).when(packageLoader).processFile(MOCK_FILES.get(0)); + Mockito.doReturn(MOCK_JAR_ENTRIES).when(packageLoader).extractEntries(Mockito.any()); + + final ModuleInfo info = packageLoader.loadPackage(MOCK_FILES.get(0)); + + Mockito.verify(packageLoader, Mockito.times(1)).processFile(Mockito.any()); + Assert.assertTrue(info.getClassNames().size() == MOCK_JAR_ENTRIES.size()); + Assert.assertArrayEquals(classes.toArray(), info.getClassNames().toArray()); + Assert.assertSame(MOCK_FILES.get(0), info.getFile()); + Assert.assertSame(jarFile, info.getJarFile()); + Assert.assertSame(JarModuleExportStrategy.class, info.getExportStrategy().getClass()); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarFileTestUtils.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarFileTestUtils.java deleted file mode 100644 index 3237c4c..0000000 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarFileTestUtils.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.instrumentation.preprocess.util; - -import org.junit.rules.TemporaryFolder; - -import java.io.File; -import java.io.FileOutputStream; -import java.util.jar.JarOutputStream; -import java.util.zip.ZipEntry; - -public class JarFileTestUtils { - public static File createJar(TemporaryFolder temporaryFolder, String fileName, String... entries) throws Exception { - File file = temporaryFolder.newFile(fileName + ".jar"); - try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { - try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { - for (String entry : entries) { - //write a sentinal file with the same name as the jar, to test if it becomes readable by getResource. - jarOutputStream.putNextEntry(new ZipEntry(entry)); - jarOutputStream.write(entry.getBytes()); - jarOutputStream.closeEntry(); - } - } - } - return file; - } -} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java index 86e4148..18cca97 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java @@ -62,15 +62,6 @@ public static List makeMockJarEntries() { return list; } - public static JarFile makeMockJarFile(){ - JarFile file = Mockito.mock(JarFile.class); - - Enumeration e = Collections.enumeration(makeMockJarEntriesWithPath()); - Mockito.when(file.entries()).thenReturn(e); - - return file; - } - public static Map makeInstrumentedClassesMap() { final Map classes = new HashMap<>(); final InstrumentedClassState stateOne = new InstrumentedClassState("installable_a", new byte[]{12}); @@ -83,6 +74,13 @@ public static Map makeInstrumentedClassesMap() { return classes; } + public static List makeMockFiles() { + return Arrays.asList(new File("file_a"), + new File("file_b"), + new File("file_c")); + } + + public static List makeMockPathsWithDuplicates() { return Arrays.asList("path_a", "path_a", "path_b", "path_c"); } @@ -115,16 +113,22 @@ public static DynamicType makeMockDynamicType(){ return type; } - public static ModuleInfo makeMockModuleInfo(){ + public static ModuleInfo makeMockPackageInfo(){ final ModuleInfo info = Mockito.mock(ModuleInfo.class); final File mockFile = Mockito.mock(File.class); + final JarFile mockJarFile = Mockito.mock(JarFile.class); final ModuleExportStrategy mockStrategy = Mockito.mock(ModuleExportStrategy.class); Mockito.lenient().when(info.getFile()).thenReturn(mockFile); + Mockito.lenient().when(info.getJarFile()).thenReturn(mockJarFile); + Mockito.lenient().when(mockJarFile.getName()).thenReturn("mock.jar"); Mockito.lenient().when(info.getExportStrategy()).thenReturn(mockStrategy); Mockito.lenient().when(mockFile.getName()).thenReturn("mock.jar"); + final Enumeration entries = Collections.enumeration(makeMockJarEntriesWithPath()); + Mockito.lenient().when(mockJarFile.entries()).thenReturn(entries); + return info; } } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java index 3a03cf6..767a380 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java @@ -27,6 +27,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import java.util.stream.Collectors; /** @@ -43,13 +44,30 @@ public class TransactionContext { public static final String UNINITIALIZED_TRANSACTION_CONTEXT_VALUE = "disco_null_id"; private static final String REFERENCE_COUNTER_KEY = "$amazon.discoRefCounterKey"; + private static final ThreadLocal> transactionContext = ThreadLocal.withInitial(new TransactionContextFactory()); - private static final ThreadLocal> transactionContext = ThreadLocal.withInitial( - ()-> { - ConcurrentMap map = new ConcurrentHashMap<>(); - map.put(TRANSACTION_ID_KEY, new MetadataItem(UNINITIALIZED_TRANSACTION_CONTEXT_VALUE)); - return map; - }); + /** + * This class was created to solve a null pointer exception when deploying a service using a statically instrumented JDK. The TransactionContext + * along with other dependency classes to enable concurrency support has to be injected to the java.base module for Java 9+ and loaded while the + * JVM is still bootstrapping itself by initializing primordial classes such as Thread. + * + * At this stage of the program execution, the JVM is unable to handle lambda expressions such as the one passed to {@link ThreadLocal#withInitial(Supplier)}. + * To remedy this shortcoming, a class that explicitly extends {@link Supplier} has been implemented and initialized and used to populate {@link #transactionContext} + * instead of using an inline lambda expression. + */ + static class TransactionContextFactory implements Supplier> { + /** + * returns a ConcurrentMap with a default {@link #UNINITIALIZED_TRANSACTION_CONTEXT_VALUE value} for the key {@link #TRANSACTION_ID_KEY} + * + * @return ThreadLocal variable which is a {@link ConcurrentMap} + */ + @Override + public ConcurrentMap get() { + ConcurrentMap map = new ConcurrentHashMap<>(); + map.put(TransactionContext.TRANSACTION_ID_KEY, new MetadataItem(TransactionContext.UNINITIALIZED_TRANSACTION_CONTEXT_VALUE)); + return map; + } + } /** * For internal use, retrieves the internal reference counter. From 98aac71b2e798cad3b28d26a979c95127d0e921f Mon Sep 17 00:00:00 2001 From: Jatin Kumar Date: Mon, 10 Aug 2020 22:22:42 -0700 Subject: [PATCH 37/45] Updrading byte-buddy-dep and asm versions --- .../disco-java-agent-inject-api/build.gradle.kts | 4 ++-- .../disco-java-agent-plugin-api/build.gradle.kts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts b/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts index 2499612..df64f9a 100644 --- a/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts @@ -20,8 +20,8 @@ plugins { dependencies { //we use the ByteBuddyAgent for an install-after-startup injection strategy, but do not want to inadvertently //pull all of BB into the client's code. - implementation("net.bytebuddy", "byte-buddy-agent", "1.9.12") - testImplementation("net.bytebuddy", "byte-buddy-dep", "1.9.12") + implementation("net.bytebuddy", "byte-buddy-agent", "1.10.14") + testImplementation("net.bytebuddy", "byte-buddy-dep", "1.10.14") } configure { diff --git a/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts b/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts index a89f8ff..fc00507 100644 --- a/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts @@ -15,10 +15,10 @@ dependencies { //TODO update BB and ASM to latest - api("net.bytebuddy", "byte-buddy-dep", "1.9.12") - implementation("org.ow2.asm", "asm", "7.1") - implementation("org.ow2.asm", "asm-commons", "7.1") - implementation("org.ow2.asm", "asm-tree", "7.1") + api("net.bytebuddy", "byte-buddy-dep", "1.10.14") + implementation("org.ow2.asm", "asm", "8.0.1") + implementation("org.ow2.asm", "asm-commons", "8.0.1") + implementation("org.ow2.asm", "asm-tree", "8.0.1") } configure { From 5129ec996c974e4967a6f91d3e9236cd91a71e7a Mon Sep 17 00:00:00 2001 From: Connell Date: Wed, 12 Aug 2020 14:30:18 -0700 Subject: [PATCH 38/45] Add new test to stress situations where user code has its own decoration mechanisms around executors. Fix ExecutorService interceptor to use a reentrancy check --- .../ApacheHttpClientInterceptor.java | 2 +- .../concurrent/ExecutorServiceTests.java | 55 +++++++++++++- .../source/UserDecoratedExecutorFactory.java | 76 +++++++++++++++++++ .../agent/concurrent/ExecutorInterceptor.java | 20 +++++ .../MethodInterceptionCounter.java | 2 +- .../concurrent/ExecutorInterceptorTests.java | 2 + .../MethodInterceptionCounterTests.java | 3 +- 7 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/UserDecoratedExecutorFactory.java rename {disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils => disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception}/MethodInterceptionCounter.java (97%) rename {disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils => disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception}/MethodInterceptionCounterTests.java (94%) diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java index 691057e..c89f4d1 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptor.java @@ -19,7 +19,7 @@ import software.amazon.disco.agent.web.apache.event.ApacheEventFactory; import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; import software.amazon.disco.agent.web.apache.utils.HttpResponseAccessor; -import software.amazon.disco.agent.web.apache.utils.MethodInterceptionCounter; +import software.amazon.disco.agent.interception.MethodInterceptionCounter; import software.amazon.disco.agent.event.EventBus; import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent; import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; diff --git a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java index c7b389f..1105efb 100644 --- a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java +++ b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java @@ -24,6 +24,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.*; import java.util.stream.Collectors; @@ -35,7 +36,8 @@ public class ExecutorServiceTests { private static final List executorServiceFactories = Arrays.asList( new FixedThreadPoolExecutorServiceFactory(), - new ScheduledThreadPoolExecutorFactory() + new ScheduledThreadPoolExecutorFactory(), + new UserDecoratedExecutorFactory() ); static abstract class Base { @@ -43,13 +45,15 @@ static abstract class Base { protected static final String result = "Result"; @Before - public void before() { + public void before() throws Exception { TestableConcurrencyObjectImpl.before(); + testMethodInterceptionCounter(); } @After - public void after() { + public void after() throws Exception { TestableConcurrencyObjectImpl.after(); + testMethodInterceptionCounter(); } protected void testBeforeInvocation(TestableConcurrencyObject testable) { @@ -230,7 +234,7 @@ public void testInvokeAnyCallableWithTimeout() throws Exception { } } - class SubmitRunnableWhenThrows extends Base { + public static class SubmitRunnableWhenThrows extends Base { @Rule public ForceConcurrency.RetryRule retry = new ForceConcurrency.RetryRule(); @@ -261,4 +265,47 @@ public void run() { } } } + + @Test + public void testSubmitRunnableToUserDecoratedExecutorFactoryWhenExecuteExitsNormally() throws Exception { + testMethodInterceptionCounter(); + + Runnable r = ()->{}; + ExecutorService executorService = new UserDecoratedExecutorFactory.UserDecoratedExecutor(); + + Future f = executorService.submit(r); + executorService.shutdown(); + executorService.awaitTermination(1, TimeUnit.DAYS); + f.get(); + + testMethodInterceptionCounter(); + } + + @Test + public void testSubmitRunnableToUserDecoratedExecutorFactoryWhenExecuteThrows() throws Exception { + testMethodInterceptionCounter(); + + Runnable r = ()->{}; + ExecutorService executorService = new UserDecoratedExecutorFactory.UserDecoratedExecutor(new RuntimeException()); + + try { + Future f = executorService.submit(r); + executorService.shutdown(); + executorService.awaitTermination(1, TimeUnit.DAYS); + f.get(); + } catch (RuntimeException e) { + + } + + testMethodInterceptionCounter(); + } + + private static void testMethodInterceptionCounter() throws Exception { + //invasive reflection not ideal here, but we need to check that the exit advice was called, ensuring that + //reentrancy counter was left at zero + Object interceptionCounter = Class.forName("software.amazon.disco.agent.concurrent.ExecutorInterceptor$ExecuteAdvice").getDeclaredField("interceptionCounter").get(null); + Method hasIntercepted = Class.forName("software.amazon.disco.agent.interception.MethodInterceptionCounter").getDeclaredMethod("hasIntercepted"); + boolean result = (boolean)hasIntercepted.invoke(interceptionCounter); + Assert.assertFalse(result); + } } diff --git a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/UserDecoratedExecutorFactory.java b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/UserDecoratedExecutorFactory.java new file mode 100644 index 0000000..867fefb --- /dev/null +++ b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/source/UserDecoratedExecutorFactory.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.integtest.concurrent.source; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Represents user scenarios where they already employ a decoration in the execute() method of a custom Executor. + * We want to ensure that if the user has any instanceof checks against *their* type of Decorated runnable, that the disco + * runnable is 'underneath' it such that their checks do not suddenly fail. + */ +public class UserDecoratedExecutorFactory implements ExecutorServiceFactory { + + @Override + public ExecutorService createExecutorService() { + return new UserDecoratedExecutor(); + } + + public static class UserDecoratedExecutor extends ThreadPoolExecutor { + private final RuntimeException exception; + + public UserDecoratedExecutor(RuntimeException toThrow) { + super(2, 2, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1)); + exception = toThrow; + } + + public UserDecoratedExecutor() { + this(null); + } + + @Override + protected void beforeExecute(Thread t, Runnable r) { + if (!(r instanceof UserDecoratedRunnable)) { + //customer code expects it to be one of these, not the disco DecoratedRunnable which should be 'beneath' their abstraction, not above it. + throw new IllegalStateException(); + } + super.beforeExecute(t, r); + } + + @Override + public void execute(Runnable command) { + super.execute(new UserDecoratedRunnable(command)); + if (exception != null) { + throw exception; + } + } + + static class UserDecoratedRunnable implements Runnable { + Runnable target; + UserDecoratedRunnable(Runnable target) { + this.target = target; + } + + @Override + public void run() { + target.run(); + } + } + } +} diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ExecutorInterceptor.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ExecutorInterceptor.java index a287f05..3cb9d3e 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ExecutorInterceptor.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ExecutorInterceptor.java @@ -17,6 +17,7 @@ import software.amazon.disco.agent.concurrent.decorate.DecoratedRunnable; import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.agent.interception.MethodInterceptionCounter; import software.amazon.disco.agent.logging.LogManager; import software.amazon.disco.agent.logging.Logger; import net.bytebuddy.agent.builder.AgentBuilder; @@ -61,6 +62,8 @@ public AgentBuilder install(AgentBuilder agentBuilder) { * Advice class to decorate the execute() method of any implementation of the Executor interface */ public static class ExecuteAdvice { + public static final MethodInterceptionCounter interceptionCounter = new MethodInterceptionCounter(); + /** * ByteBuddy advice method to capture the Runnable before it is used, and decorate it. * @@ -85,9 +88,26 @@ public static void onMethodEnter(@Advice.Argument(value = 0, readOnly = false) R * @return the decorated command */ public static Runnable methodEnter(Runnable command) { + boolean reentrant = interceptionCounter.hasIntercepted(); + interceptionCounter.increment(); + if (reentrant) { + return command; + } return DecoratedRunnable.maybeCreate(command); } + /** + * Advice method to finalize the interception counter, making sure that nested calls of execute() will return it to zero + */ + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void onMethodExit() { + methodExit(); + } + + public static void methodExit() { + interceptionCounter.decrement(); + } + /** * Under normal circumstances should not be called, but for debugging, we call out to a 'real' method * @param t the throwable which was thrown by the advice diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/MethodInterceptionCounter.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/MethodInterceptionCounter.java similarity index 97% rename from disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/MethodInterceptionCounter.java rename to disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/MethodInterceptionCounter.java index 76593af..a98dc04 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/utils/MethodInterceptionCounter.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/interception/MethodInterceptionCounter.java @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package software.amazon.disco.agent.web.apache.utils; +package software.amazon.disco.agent.interception; import java.util.concurrent.atomic.AtomicInteger; diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ExecutorInterceptorTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ExecutorInterceptorTests.java index eeb6aba..2523101 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ExecutorInterceptorTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ExecutorInterceptorTests.java @@ -85,6 +85,7 @@ public void testInstall() { @Test public void testExecuteAdvice() { ExecutorInterceptor.ExecuteAdvice.onMethodEnter(Mockito.mock(Runnable.class)); + ExecutorInterceptor.ExecuteAdvice.onMethodExit(); //ensure interception counter is decremented } @Test @@ -97,6 +98,7 @@ public void testExecuteAdviceDecorates() { Runnable r = Mockito.mock(Runnable.class); Runnable d = ExecutorInterceptor.ExecuteAdvice.methodEnter(r); Assert.assertTrue(d instanceof DecoratedRunnable); + ExecutorInterceptor.ExecuteAdvice.onMethodExit(); //ensure interception counter is decremented } } diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/MethodInterceptionCounterTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/MethodInterceptionCounterTests.java similarity index 94% rename from disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/MethodInterceptionCounterTests.java rename to disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/MethodInterceptionCounterTests.java index a9e1dbd..6281c75 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/utils/MethodInterceptionCounterTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/interception/MethodInterceptionCounterTests.java @@ -13,10 +13,11 @@ * permissions and limitations under the License. */ -package software.amazon.disco.agent.web.apache.utils; +package software.amazon.disco.agent.interception; import org.junit.Before; import org.junit.Test; +import software.amazon.disco.agent.interception.MethodInterceptionCounter; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; From a7de0b9d2be6f3b0f6920e2f2b10a697fcf6d306 Mon Sep 17 00:00:00 2001 From: William Armiros Date: Mon, 10 Aug 2020 14:21:32 -0700 Subject: [PATCH 39/45] setup project structure of aws interceptors --- README.md | 1 + build.gradle.kts | 40 ++++ .../CODE_OF_CONDUCT.md | 0 .../CONTRIBUTING.md | 0 disco-java-agent-aws/LICENSE | 202 ++++++++++++++++++ disco-java-agent-aws/NOTICE | 2 + disco-java-agent-aws/README.md | 20 ++ disco-java-agent-aws/build.gradle.kts | 28 +++ .../disco-java-agent-aws-api/LICENSE | 202 ++++++++++++++++++ .../disco-java-agent-aws-api/NOTICE | 2 + .../disco-java-agent-aws-api/README.md | 5 + .../disco-java-agent-aws-api/build.gradle.kts | 26 +++ .../gradle.properties | 16 ++ .../amazon/disco/agent/event/.gitkeep | 1 + .../disco-java-agent-aws-plugin/LICENSE | 202 ++++++++++++++++++ .../disco-java-agent-aws-plugin/NOTICE | 2 + .../disco-java-agent-aws-plugin/README.md | 19 ++ .../build.gradle.kts | 34 +++ .../gradle.properties | 16 ++ .../java/software/amazon/disco/agent/.gitkeep | 1 + disco-java-agent-aws/gradle.properties | 16 ++ .../java/software/amazon/disco/agent/.gitkeep | 1 + .../java/software/amazon/disco/agent/.gitkeep | 1 + .../build.gradle.kts | 38 ---- .../CODE_OF_CONDUCT.md | 4 - .../CONTRIBUTING.md | 61 ------ .../build.gradle.kts | 33 --- settings.gradle.kts | 4 + 28 files changed, 841 insertions(+), 136 deletions(-) rename {disco-java-agent-sql/disco-java-agent-sql-plugin => disco-java-agent-aws}/CODE_OF_CONDUCT.md (100%) rename {disco-java-agent-sql/disco-java-agent-sql-plugin => disco-java-agent-aws}/CONTRIBUTING.md (100%) create mode 100644 disco-java-agent-aws/LICENSE create mode 100644 disco-java-agent-aws/NOTICE create mode 100644 disco-java-agent-aws/README.md create mode 100644 disco-java-agent-aws/build.gradle.kts create mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/LICENSE create mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/NOTICE create mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/README.md create mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/build.gradle.kts create mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/gradle.properties create mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/.gitkeep create mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/LICENSE create mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/NOTICE create mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/README.md create mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts create mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/gradle.properties create mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/.gitkeep create mode 100644 disco-java-agent-aws/gradle.properties create mode 100644 disco-java-agent-aws/src/main/java/software/amazon/disco/agent/.gitkeep create mode 100644 disco-java-agent-aws/src/test/java/software/amazon/disco/agent/.gitkeep delete mode 100644 disco-java-agent-web/disco-java-agent-web-plugin/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent-web/disco-java-agent-web-plugin/CONTRIBUTING.md diff --git a/README.md b/README.md index 9bbd729..14cd1a2 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ a few layers and families of projects in here: 1. A facility to 'Inject' a Disco Agent into managed runtimes like AWS Lambda 1. A Plugin to support Servlets and Apache HTTP clients, in disco-java-agent-web-plugin 1. A Plugin to support SQL connections & queries using JDBC, in disco-java-agent-sql-plugin +1. A Plugin to support requests made with the AWS SDK for Java, in disco-java-agent-aws-plugin 1. Example code in anything with '-example' in the project name. 1. Tests in anything with '-test' in the project name. diff --git a/build.gradle.kts b/build.gradle.kts index 410cec1..484d8c6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -71,6 +71,46 @@ subprojects { } } + // This block only applies to plugin modules, as determined by the existence of a "-plugin" suffix + if (project.name.endsWith("-plugin")) { + // Remove "-plugin" suffix to get corresponding library name + val libraryName = ":" + project.name.subSequence(0, project.name.length - 7) + val ver = project.version + + // Configure dependencies common to plugins + dependencies { + runtimeOnly(project(libraryName)) { + // By setting the isTransitive flag false, we take only what is described by the above project, and not + // its entire closure of transitive dependencies (i.e. all of Core, all of Bytebuddy, etc) + // this makes our generated Jar minimal, containing only our source files, and our manifest. All those + // other dependencies are expected to be in the base agent, which loads this plugin. + isTransitive = false + } + + // Test target is integ tests for Disco plugins. Some classes in the integ tests also self-test via + // little unit tests during testrun. + testImplementation(project(":disco-java-agent:disco-java-agent-api")) + testImplementation("org.mockito", "mockito-core", "1.+") + } + + // Configure integ tests, which need a loaded agent, and the loaded plugin + tasks.test { + // explicitly remove the runtime classpath from the tests since they are integ tests, and may not access the + // dependency we acquired in order to build the plugin, namely the library jar for this plugin which makes reference + // to byte buddy classes which have NOT been relocated by a shadowJar rule. Discovering those unrelocated classes + // would not be possible in a real client installation, and would cause plugin loading to fail. + classpath = classpath.minus(configurations.runtimeClasspath.get()) + + //load the agent for the tests, and have it discover the plugin + jvmArgs("-javaagent:../../disco-java-agent/disco-java-agent/build/libs/disco-java-agent-$ver.jar=pluginPath=./build/libs:extraverbose") + + //we do not take any normal compile/runtime dependency on this, but it must be built first since the above jvmArg + //refers to its built artifact. + dependsOn(":disco-java-agent:disco-java-agent:build") + dependsOn("$libraryName:${project.name}:assemble") + } + } + //we publish everything except example subprojects to maven. Projects which desire to be published to maven express the intent //via a property called simply 'maven' in their gradle.properties file (if it exists at all). //Each package to be published still needs a small amount of boilerplate to express whether is is a 'normal' diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/CODE_OF_CONDUCT.md b/disco-java-agent-aws/CODE_OF_CONDUCT.md similarity index 100% rename from disco-java-agent-sql/disco-java-agent-sql-plugin/CODE_OF_CONDUCT.md rename to disco-java-agent-aws/CODE_OF_CONDUCT.md diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/CONTRIBUTING.md b/disco-java-agent-aws/CONTRIBUTING.md similarity index 100% rename from disco-java-agent-sql/disco-java-agent-sql-plugin/CONTRIBUTING.md rename to disco-java-agent-aws/CONTRIBUTING.md diff --git a/disco-java-agent-aws/LICENSE b/disco-java-agent-aws/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/disco-java-agent-aws/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/disco-java-agent-aws/NOTICE b/disco-java-agent-aws/NOTICE new file mode 100644 index 0000000..fbd62dc --- /dev/null +++ b/disco-java-agent-aws/NOTICE @@ -0,0 +1,2 @@ +DiSCo +Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/disco-java-agent-aws/README.md b/disco-java-agent-aws/README.md new file mode 100644 index 0000000..22ee307 --- /dev/null +++ b/disco-java-agent-aws/README.md @@ -0,0 +1,20 @@ +## Disco 'AWS SDK' Support + +Serving as both an example of how to author a Disco library/plugin/api, and also as a usable +Event producer for requests made using versions 1 and 2 of the AWS SDK for Java, this subproject is laid out as follows: + +1. In this folder, the Installables to intercept AWS SDK requests & responses, and issue appropriate Event Bus Events. +1. In the disco-java-agent-aws-plugin subfolder, a proper Disco plugin, bundled as a plugin JAR file with Manifest. +1. In the disco-java-agent-aws-api subfolder, a collection of Event classes which are implemented & published by +the intstallables in this package. + +See the READMEs of the submodules for more information on each. + +## Feature Status + +TODO + +## Package description + +`AwsSupport` is a Disco Package that can be installed by standalone Agents to gain interception and +event publication for the features described above. diff --git a/disco-java-agent-aws/build.gradle.kts b/disco-java-agent-aws/build.gradle.kts new file mode 100644 index 0000000..d7e079c --- /dev/null +++ b/disco-java-agent-aws/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +dependencies { + implementation(project(":disco-java-agent-aws:disco-java-agent-aws-api")) + implementation(project(":disco-java-agent:disco-java-agent-core")) + testImplementation("org.mockito", "mockito-core", "1.+") +} + +configure { + publications { + named("maven") { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/LICENSE b/disco-java-agent-aws/disco-java-agent-aws-api/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-api/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/NOTICE b/disco-java-agent-aws/disco-java-agent-aws-api/NOTICE new file mode 100644 index 0000000..fbd62dc --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-api/NOTICE @@ -0,0 +1,2 @@ +DiSCo +Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/README.md b/disco-java-agent-aws/disco-java-agent-aws-api/README.md new file mode 100644 index 0000000..c1224b2 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-api/README.md @@ -0,0 +1,5 @@ +## Disco 'AWS' API Package + +This package contains classes and interfaces which are extended and implemented by the `disco-java-agent-aws` library +as Disco Events. Consumers of Disco AWS Support can optionally depend on this package to cast Events received from the +event bus to these types and not lose any AWS-SDK-specific data from downcasting. diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/build.gradle.kts b/disco-java-agent-aws/disco-java-agent-aws-api/build.gradle.kts new file mode 100644 index 0000000..cf7319f --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-api/build.gradle.kts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +dependencies { + implementation(project(":disco-java-agent:disco-java-agent-api")) +} + +configure { + publications { + named("maven") { + artifact(tasks.jar.get()) + } + } +} diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/gradle.properties b/disco-java-agent-aws/disco-java-agent-aws-api/gradle.properties new file mode 100644 index 0000000..d861bc6 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-api/gradle.properties @@ -0,0 +1,16 @@ +# +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# + +maven = true \ No newline at end of file diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/.gitkeep b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/.gitkeep new file mode 100644 index 0000000..1a48e48 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/.gitkeep @@ -0,0 +1 @@ +TODO: Add AWS Events \ No newline at end of file diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/LICENSE b/disco-java-agent-aws/disco-java-agent-aws-plugin/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/NOTICE b/disco-java-agent-aws/disco-java-agent-aws-plugin/NOTICE new file mode 100644 index 0000000..fbd62dc --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/NOTICE @@ -0,0 +1,2 @@ +DiSCo +Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/README.md b/disco-java-agent-aws/disco-java-agent-aws-plugin/README.md new file mode 100644 index 0000000..a64e848 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/README.md @@ -0,0 +1,19 @@ +## Disco 'AWS' Service Support Plugin + +This is a plugin built from the source in the folder above (and the source in API sibling module), including a build +rule to output a well-formed Disco plugin. + +### Manifest generation + +The build.gradle.kts file contains a build rule to generate an appropriate MANIFEST + +### Dependency shading + +Inherited from a top level build.gradle.kts file in the top level project, the ByteBuddy and ASM +dependencies are repackaged in agreement with the expectations of the disco-java-agent. + +### Integ Tests + +The test target in build.gradle.kts is configured to apply the disco-java-agent via an argument given to the +invocation of java, supplied to which is a pluginPath pointing to the output folder where the built +disco-java-agent-aws-plugin plugin JAR file can be found. Without both of these, the tests will fail. \ No newline at end of file diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts b/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts new file mode 100644 index 0000000..e01f9a3 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +plugins { + id("com.github.johnrengelman.shadow") +} + +tasks.shadowJar { + manifest { + attributes(mapOf( + "Disco-Installable-Classes" to "software.amazon.disco.agent.web.AWSSupport" + )) + } +} + +configure { + publications { + named("maven") { + artifact(tasks.jar.get()) + } + } +} diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/gradle.properties b/disco-java-agent-aws/disco-java-agent-aws-plugin/gradle.properties new file mode 100644 index 0000000..d861bc6 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/gradle.properties @@ -0,0 +1,16 @@ +# +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# + +maven = true \ No newline at end of file diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/.gitkeep b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/.gitkeep new file mode 100644 index 0000000..bc9b219 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/.gitkeep @@ -0,0 +1 @@ +TODO: Replace with integ tests \ No newline at end of file diff --git a/disco-java-agent-aws/gradle.properties b/disco-java-agent-aws/gradle.properties new file mode 100644 index 0000000..d861bc6 --- /dev/null +++ b/disco-java-agent-aws/gradle.properties @@ -0,0 +1,16 @@ +# +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# + +maven = true \ No newline at end of file diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/.gitkeep b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/.gitkeep new file mode 100644 index 0000000..3b67524 --- /dev/null +++ b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/.gitkeep @@ -0,0 +1 @@ +TODO Add source code \ No newline at end of file diff --git a/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/.gitkeep b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/.gitkeep new file mode 100644 index 0000000..cc06484 --- /dev/null +++ b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/.gitkeep @@ -0,0 +1 @@ +TODO add unit tests \ No newline at end of file diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts b/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts index 9e47df3..0ddc4be 100644 --- a/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts @@ -17,25 +17,6 @@ plugins { id("com.github.johnrengelman.shadow") } -dependencies { - // TODO: Refactor this block and other common plugin build logic to top-level build.gradle after deciding - // a safe way to check whether a subproject represents a plugin - runtimeOnly(project(":disco-java-agent-sql")) { - //by setting this flag false, we take only what is described by the above project, and not its entire - //closure of transitive dependencies (i.e. all of Core, all of Bytebuddy, etc) - //this makes our generated Jar minimal, containing only our source files, and our manifest. All those other - //dependencies are expected to be in the base agent, which loads this plugin. - //Ideally we would have a test for this which inspects the final Jar's content, but it can be reviewed manually - //on the command line with "jar -tf disco-java-agent-sql-plugin.jar" - isTransitive = false - } - - //Test target is integ tests for this plugin. Some classes in the integ tests also self-test via little unit tests during this - //testrun. - testImplementation(project(":disco-java-agent:disco-java-agent-api")) - testImplementation("org.mockito", "mockito-core", "1.+") -} - tasks.shadowJar { manifest { attributes(mapOf( @@ -44,25 +25,6 @@ tasks.shadowJar { } } -val ver = project.version - -//integ testing needs a loaded agent, and the loaded plugin -tasks.test { - //explicitly remove the runtime classpath from the tests since they are integ tests, and may not access the - //dependency we acquired in order to build the plugin, namely the disco-java-agent-sql jar which makes reference - //to byte buddy classes which have NOT been relocated by a shadowJar rule. Discovering those unrelocated classes - //would not be possible in a real client installation, and would cause plugin loading to fail. - classpath = classpath.minus(configurations.runtimeClasspath.get()) - - //load the agent for the tests, and have it discover the web plugin - jvmArgs("-javaagent:../../disco-java-agent/disco-java-agent/build/libs/disco-java-agent-"+ver+".jar=pluginPath=./build/libs:extraverbose") - - //we do not take any normal compile/runtime dependency on this, but it must be built first since the above jvmArg - //refers to its built artifact. - dependsOn(":disco-java-agent:disco-java-agent:build") - dependsOn(":disco-java-agent-sql:disco-java-agent-sql-plugin:assemble") -} - configure { publications { named("maven") { diff --git a/disco-java-agent-web/disco-java-agent-web-plugin/CODE_OF_CONDUCT.md b/disco-java-agent-web/disco-java-agent-web-plugin/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent-web/disco-java-agent-web-plugin/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent-web/disco-java-agent-web-plugin/CONTRIBUTING.md b/disco-java-agent-web/disco-java-agent-web-plugin/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent-web/disco-java-agent-web-plugin/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts b/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts index 3107e0b..d58c290 100644 --- a/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts +++ b/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts @@ -18,20 +18,6 @@ plugins { } dependencies { - runtimeOnly(project(":disco-java-agent-web")) { - //by setting this flag false, we take only what is described by the above project, and not its entire - //closure of transitive dependencies (i.e. all of Core, all of Bytebuddy, etc) - //this makes our generated Jar minimal, containing only our source files, and our manifest. All those other - //dependencies are expected to be in the base agent, which loads this plugin. - //Ideally we would have a test for this which inspects the final Jar's content, but it can be reviewed manually - //on the command line with "jar -tf disco-java-agent-web-plugin.jar" - isTransitive = false - } - - //Test target is integ tests for this plugin. Some classes in the integ tests also self-test via little unit tests during this - //testrun. - testImplementation(project(":disco-java-agent:disco-java-agent-api")) - testImplementation("org.mockito", "mockito-core", "1.+") testImplementation("javax.servlet", "javax.servlet-api", "3.0.1") testImplementation("org.apache.httpcomponents", "httpclient", "4.5.10") } @@ -44,25 +30,6 @@ tasks.shadowJar { } } -val ver = project.version - -//integ testing needs a loaded agent, and the loaded plugin -tasks.test { - //explicitly remove the runtime classpath from the tests since they are integ tests, and may not access the - //dependency we acquired in order to build the plugin, namely the disco-java-agent-web jar which makes reference - //to byte buddy classes which have NOT been relocated by a shadowJar rule. Discovering those unrelocated classes - //would not be possible in a real client installation, and would cause plugin loading to fail. - classpath = classpath.minus(configurations.runtimeClasspath.get()) - - //load the agent for the tests, and have it discover the web plugin - jvmArgs("-javaagent:../../disco-java-agent/disco-java-agent/build/libs/disco-java-agent-"+ver+".jar=pluginPath=./build/libs:extraverbose") - - //we do not take any normal compile/runtime dependency on this, but it must be built first since the above jvmArg - //refers to its built artifact. - dependsOn(":disco-java-agent:disco-java-agent:build") - dependsOn(":disco-java-agent-web:disco-java-agent-web-plugin:assemble") -} - configure { publications { named("maven") { diff --git a/settings.gradle.kts b/settings.gradle.kts index 06f0fea..ef45f5a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,10 @@ include("disco-java-agent:disco-java-agent-api") include("disco-java-agent:disco-java-agent-core") include("disco-java-agent:disco-java-agent-inject-api") +include("disco-java-agent-aws") +include("disco-java-agent-aws:disco-java-agent-aws-api") +include("disco-java-agent-aws:disco-java-agent-aws-plugin") + include("disco-java-agent-web") include("disco-java-agent-web:disco-java-agent-web-plugin") From 5df6a6b87bb783b43792a288551c62afd10b63f6 Mon Sep 17 00:00:00 2001 From: Connell Date: Tue, 18 Aug 2020 10:55:54 -0700 Subject: [PATCH 40/45] Fix TX propagation logic in cases where a task submitted to an Executor itself submits a task, and the Executor deems that the same thread can be reused for each --- .../concurrent/ExecutorServiceTests.java | 44 ++++++++++++++++--- .../agent/concurrent/ConcurrentUtils.java | 18 +++++--- .../agent/concurrent/TransactionContext.java | 13 +++++- .../agent/concurrent/decorate/Decorated.java | 14 +++--- .../concurrent/ConcurrentUtilsTests.java | 29 +++++++----- .../ThreadSubclassInterceptorTests.java | 2 +- .../concurrent/TransactionContextTests.java | 16 +++---- .../decorate/DecoratedCallableTests.java | 6 +-- .../concurrent/decorate/DecoratedTests.java | 4 +- 9 files changed, 104 insertions(+), 42 deletions(-) diff --git a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java index 1105efb..92ce845 100644 --- a/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java +++ b/disco-java-agent/disco-java-agent-core/src/integ/java/software/amazon/disco/agent/integtest/concurrent/ExecutorServiceTests.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import software.amazon.disco.agent.reflect.concurrent.TransactionContext; import java.lang.reflect.Method; import java.util.*; @@ -244,7 +245,7 @@ public void testSubmitRunnableWhenThrows() throws Exception { r.testBeforeInvocation(); executorService = Executors.newFixedThreadPool(2); - Future f = executorService.submit(r); + Future f = executorService.submit(r); executorService.shutdown(); executorService.awaitTermination(1, TimeUnit.DAYS); Throwable thrown = null; @@ -258,7 +259,7 @@ public void testSubmitRunnableWhenThrows() throws Exception { r.testAfterConcurrentInvocation(); } - class ThrowingRunnable extends TestableConcurrencyObjectImpl.WhichThrows implements Runnable { + static class ThrowingRunnable extends TestableConcurrencyObjectImpl.WhichThrows implements Runnable { @Override public void run() { perform(); @@ -266,6 +267,39 @@ public void run() { } } + public static class SubmitRunnableNestedWhenExecutorReusesThread extends RunnableBase { + @Rule + public ForceConcurrency.RetryRule retry = new ForceConcurrency.RetryRule(); + + @Test + public void testSubmitRunnableNestedWhenExecutorReusesThread() throws Exception { + ExecutorService e = Executors.newFixedThreadPool(1); + AbstractQueue> futures = new ConcurrentLinkedQueue<>(); + long[] threadIdStack = new long[3]; + threadIdStack[0] = Thread.currentThread().getId(); + TransactionContext.putMetadata("foo", "bar"); + futures.add(e.submit(() -> { + TransactionContext.putMetadata("foo2", "bar2"); + threadIdStack[1] = Thread.currentThread().getId(); + futures.add(e.submit(() -> { + threadIdStack[2] = Thread.currentThread().getId(); + Assert.assertEquals("bar", TransactionContext.getMetadata("foo")); + Assert.assertEquals("bar2", TransactionContext.getMetadata("foo2")); + })); + })); + + Future f; + while ((f = futures.poll()) != null) { + f.get(1, TimeUnit.DAYS); + } + + //need to retry test in the race condition case where a thread was not in fact reused. i.e. out of the 3 thread ids, 2 (or all 3) must be the same. + if(!(threadIdStack[0]==threadIdStack[1] || threadIdStack[1]==threadIdStack[2] || threadIdStack[0]==threadIdStack[2])) { + throw new ConcurrencyCanBeRetriedException(); + } + } + } + @Test public void testSubmitRunnableToUserDecoratedExecutorFactoryWhenExecuteExitsNormally() throws Exception { testMethodInterceptionCounter(); @@ -273,7 +307,7 @@ public void testSubmitRunnableToUserDecoratedExecutorFactoryWhenExecuteExitsNorm Runnable r = ()->{}; ExecutorService executorService = new UserDecoratedExecutorFactory.UserDecoratedExecutor(); - Future f = executorService.submit(r); + Future f = executorService.submit(r); executorService.shutdown(); executorService.awaitTermination(1, TimeUnit.DAYS); f.get(); @@ -289,12 +323,12 @@ public void testSubmitRunnableToUserDecoratedExecutorFactoryWhenExecuteThrows() ExecutorService executorService = new UserDecoratedExecutorFactory.UserDecoratedExecutor(new RuntimeException()); try { - Future f = executorService.submit(r); + Future f = executorService.submit(r); executorService.shutdown(); executorService.awaitTermination(1, TimeUnit.DAYS); f.get(); } catch (RuntimeException e) { - + //do nothing } testMethodInterceptionCounter(); diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ConcurrentUtils.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ConcurrentUtils.java index 1a93e10..d7f88ae 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ConcurrentUtils.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/ConcurrentUtils.java @@ -30,33 +30,37 @@ public class ConcurrentUtils { private static Logger log = LogManager.getLogger(ConcurrentUtils.class); /** - * Propagate the transaction context, if running in a different thread than at construction time + * Propagate the transaction context, if running in a child of the ancestral thread + * @param ancestralThreadId the threadId of the thread which created the TransactionContext for this family of threads * @param parentThreadId the threadId of the thread which created the object being passed across thread boundary * @param discoTransactionContext the parent's TransactionContext map. */ - public static void set(long parentThreadId, ConcurrentMap discoTransactionContext) { + public static void set(long ancestralThreadId, long parentThreadId, ConcurrentMap discoTransactionContext) { if (discoTransactionContext == null) { - log.error("DiSCo(Core) could not propagate null context from thread id " + parentThreadId + " to thread id " + Thread.currentThread().getId()); + log.error("DiSCo(Core) could not propagate null context from thread id " + ancestralThreadId + " to thread id " + Thread.currentThread().getId()); return; } - if (Thread.currentThread().getId() != parentThreadId && !isDiscoNullId(discoTransactionContext)) { + long thisThreadId = Thread.currentThread().getId(); + if (ancestralThreadId != thisThreadId && !isDiscoNullId(discoTransactionContext)) { TransactionContext.setPrivateMetadata(discoTransactionContext); EventBus.publish(new ThreadEnterEvent("Concurrency", parentThreadId, Thread.currentThread().getId())); } } /** - * Clear the transaction context, if running in a different thread than at construction time + * Clear the transaction context, if running in a child of the ancestral thread + * @param ancestralThreadId the threadId of the thread which created the TransactionContext for this family of threads * @param parentThreadId the threadId of the thread which created the object being passed across thread boundary * @param discoTransactionContext the parent's TransactionContext map. */ - public static void clear(long parentThreadId, ConcurrentMap discoTransactionContext) { + public static void clear(long ancestralThreadId, long parentThreadId, ConcurrentMap discoTransactionContext) { if (discoTransactionContext == null) { return; } - if (Thread.currentThread().getId() != parentThreadId && !isDiscoNullId(discoTransactionContext)) { + long thisThreadId = Thread.currentThread().getId(); + if (ancestralThreadId != thisThreadId && !isDiscoNullId(discoTransactionContext)) { EventBus.publish(new ThreadExitEvent("Concurrency", parentThreadId, Thread.currentThread().getId())); TransactionContext.clear(); } diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java index 767a380..18a2259 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/TransactionContext.java @@ -41,6 +41,7 @@ public class TransactionContext { private static Logger log = LogManager.getLogger(TransactionContext.class); static final String TRANSACTION_ID_KEY = "$amazon.discoTransactionId"; + public static final String TRANSACTION_OWNING_THREAD_KEY = "$amazon.discoTransactionOwningThreadId"; public static final String UNINITIALIZED_TRANSACTION_CONTEXT_VALUE = "disco_null_id"; private static final String REFERENCE_COUNTER_KEY = "$amazon.discoRefCounterKey"; @@ -64,7 +65,8 @@ static class TransactionContextFactory implements Supplier get() { ConcurrentMap map = new ConcurrentHashMap<>(); - map.put(TransactionContext.TRANSACTION_ID_KEY, new MetadataItem(TransactionContext.UNINITIALIZED_TRANSACTION_CONTEXT_VALUE)); + map.put(TRANSACTION_ID_KEY, new MetadataItem(TransactionContext.UNINITIALIZED_TRANSACTION_CONTEXT_VALUE)); + map.put(TRANSACTION_OWNING_THREAD_KEY, new MetadataItem(Long.valueOf(-1))); return map; } } @@ -93,6 +95,7 @@ public static int create() { if (getReferenceCounter() == null || getReferenceCounter().get() <= 0) { clear(); set(UUID.randomUUID().toString()); + putMetadata(TRANSACTION_OWNING_THREAD_KEY, Long.valueOf(Thread.currentThread().getId())); transactionContext.get().put(REFERENCE_COUNTER_KEY, new MetadataItem(new AtomicInteger(0))); EventBus.publish(new TransactionBeginEvent("Core")); } @@ -214,6 +217,14 @@ public static void clearMetadataTag(String key, String tag) { } } + /** + * Queries if a given metadata key has the specified tag. The metadata must exist, which can be checked via a prior + * call to getMetadata(), checking that null is not returned. + * @param key a String to identify the data. + * @param tag a String representing the label/tag + * @return true if this metadata has the given tag. + * @throws IllegalArgumentException if no such metadata exists + */ public static boolean hasMetadataTag(String key, String tag) { if (transactionContext.get().get(key) == null) { throw new IllegalArgumentException(key + " no metadata object exists for this key"); diff --git a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/Decorated.java b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/Decorated.java index c4486b3..57486df 100644 --- a/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/Decorated.java +++ b/disco-java-agent/disco-java-agent-core/src/main/java/software/amazon/disco/agent/concurrent/decorate/Decorated.java @@ -16,6 +16,7 @@ package software.amazon.disco.agent.concurrent.decorate; import software.amazon.disco.agent.concurrent.ConcurrentUtils; +import software.amazon.disco.agent.concurrent.MetadataItem; import software.amazon.disco.agent.concurrent.TransactionContext; import java.util.concurrent.ConcurrentMap; @@ -25,28 +26,31 @@ * with extra metadata regarding thread provenance. This metadata is encapsulated in this abstraction. */ public abstract class Decorated { - protected Long parentThreadId; - ConcurrentMap parentTransactionContext; + protected Long ancestralThreadId; + protected long parentThreadId; + ConcurrentMap parentTransactionContext; /** * Construct a new object to hold thread provenance information. */ protected Decorated() { - this.parentThreadId = Thread.currentThread().getId(); this.parentTransactionContext = TransactionContext.getPrivateMetadata(); + MetadataItem data = parentTransactionContext.get(TransactionContext.TRANSACTION_OWNING_THREAD_KEY); + this.ancestralThreadId = (Long)data.get(); + this.parentThreadId = Thread.currentThread().getId(); } /** * Convenience method to call before the execution of the dispatched object method eg. run() or call() */ public void before() { - ConcurrentUtils.set(parentThreadId, parentTransactionContext); + ConcurrentUtils.set(ancestralThreadId, parentThreadId, parentTransactionContext); } /** * Convenience method to call after the execution of the dispatched object method eg. run() or call() */ public void after() { - ConcurrentUtils.clear(parentThreadId, parentTransactionContext); + ConcurrentUtils.clear(ancestralThreadId, parentThreadId, parentTransactionContext); } } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ConcurrentUtilsTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ConcurrentUtilsTests.java index c24e963..5a5a053 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ConcurrentUtilsTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ConcurrentUtilsTests.java @@ -19,6 +19,7 @@ import software.amazon.disco.agent.event.EventBus; import software.amazon.disco.agent.event.Listener; import software.amazon.disco.agent.event.ThreadEnterEvent; +import software.amazon.disco.agent.event.ThreadEvent; import software.amazon.disco.agent.event.ThreadExitEvent; import org.junit.After; import org.junit.Assert; @@ -50,10 +51,14 @@ public void testSet() { ConcurrentMap transactionContext = new ConcurrentHashMap<>(); transactionContext.put(TransactionContext.TRANSACTION_ID_KEY, new MetadataItem("id")); transactionContext.put("foo", new MetadataItem("bar")); - ConcurrentUtils.set(-1, transactionContext); + ConcurrentUtils.set(-1, 0, transactionContext); Assert.assertEquals("bar", TransactionContext.getMetadata("foo")); Assert.assertEquals(1, listener.received.size()); - Assert.assertEquals(ThreadEnterEvent.class, listener.received.iterator().next().getClass()); + Event event = listener.received.iterator().next(); + Assert.assertEquals(ThreadEnterEvent.class, event.getClass()); + ThreadEvent threadEvent = (ThreadEvent)event; + Assert.assertEquals(0, threadEvent.getParentId()); + Assert.assertEquals(Thread.currentThread().getId(), threadEvent.getChildId()); } @Test @@ -61,21 +66,25 @@ public void testClear() { ConcurrentMap transactionContext = new ConcurrentHashMap<>(); transactionContext.put(TransactionContext.TRANSACTION_ID_KEY, new MetadataItem("id")); transactionContext.put("foo", new MetadataItem("bar")); - ConcurrentUtils.clear(-1, transactionContext); + ConcurrentUtils.clear(-1, 0, transactionContext); Assert.assertNull(TransactionContext.getMetadata("foo")); Assert.assertEquals(1, listener.received.size()); - Assert.assertEquals(ThreadExitEvent.class, listener.received.iterator().next().getClass()); + Event event = listener.received.iterator().next(); + Assert.assertEquals(ThreadExitEvent.class, event.getClass()); + ThreadEvent threadEvent = (ThreadEvent)event; + Assert.assertEquals(0, threadEvent.getParentId()); + Assert.assertEquals(Thread.currentThread().getId(), threadEvent.getChildId()); } @Test public void testSetWithNullContext() { - ConcurrentUtils.set(-1, null); + ConcurrentUtils.set(-1, 0, null); Assert.assertTrue(listener.received.isEmpty()); } @Test public void testClearWithNullContext() { - ConcurrentUtils.clear(-1, null); + ConcurrentUtils.clear(-1, 0, null); Assert.assertTrue(listener.received.isEmpty()); } @@ -83,7 +92,7 @@ public void testClearWithNullContext() { public void testSetWithDefaultContext() { ConcurrentMap transactionContext = new ConcurrentHashMap<>(); transactionContext.put(TransactionContext.TRANSACTION_ID_KEY, new MetadataItem(TransactionContext.getUninitializedTransactionContextValue())); - ConcurrentUtils.set(-1, transactionContext); + ConcurrentUtils.set(-1, 0, transactionContext); Assert.assertTrue(listener.received.isEmpty()); } @@ -91,7 +100,7 @@ public void testSetWithDefaultContext() { public void testClearWithDefaultContext() { ConcurrentMap transactionContext = new ConcurrentHashMap<>(); transactionContext.put(TransactionContext.TRANSACTION_ID_KEY, new MetadataItem(TransactionContext.getUninitializedTransactionContextValue())); - ConcurrentUtils.clear(-1, transactionContext); + ConcurrentUtils.clear(-1, 0, transactionContext); Assert.assertTrue(listener.received.isEmpty()); } @@ -99,7 +108,7 @@ public void testClearWithDefaultContext() { public void testSetWithSameThreadId() { ConcurrentMap transactionContext = new ConcurrentHashMap<>(); transactionContext.put(TransactionContext.TRANSACTION_ID_KEY, new MetadataItem("id")); - ConcurrentUtils.set(Thread.currentThread().getId(), transactionContext); + ConcurrentUtils.set(Thread.currentThread().getId(), 0, transactionContext); Assert.assertTrue(listener.received.isEmpty()); } @@ -107,7 +116,7 @@ public void testSetWithSameThreadId() { public void testClearWithSameThreadId() { ConcurrentMap transactionContext = new ConcurrentHashMap<>(); transactionContext.put(TransactionContext.TRANSACTION_ID_KEY, new MetadataItem("id")); - ConcurrentUtils.clear(Thread.currentThread().getId(), transactionContext); + ConcurrentUtils.clear(Thread.currentThread().getId(), 0, transactionContext); Assert.assertTrue(listener.received.isEmpty()); } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ThreadSubclassInterceptorTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ThreadSubclassInterceptorTests.java index 34c9b2a..ae0ce9f 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ThreadSubclassInterceptorTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/ThreadSubclassInterceptorTests.java @@ -155,7 +155,7 @@ public void listen(Event e) { static class MyDecoratedThread extends DecoratedThread { public void setThreadId(long threadId) { - this.parentThreadId = threadId; + this.ancestralThreadId = threadId; } } } diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/TransactionContextTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/TransactionContextTests.java index 8992e42..f775c5a 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/TransactionContextTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/TransactionContextTests.java @@ -51,7 +51,7 @@ public void after() { @Test public void testInitialValue() { - Assert.assertEquals(1, TransactionContext.getPrivateMetadata().size()); //only TransactionId + Assert.assertEquals(2, TransactionContext.getPrivateMetadata().size()); //only TransactionId and ThreadId Assert.assertEquals(TransactionContext.UNINITIALIZED_TRANSACTION_CONTEXT_VALUE, TransactionContext.get()); } @@ -59,7 +59,7 @@ public void testInitialValue() { public void testDestroyWithNoCreate() { TransactionContext.putMetadata("foo", "bar"); TransactionContext.destroy(); - Assert.assertEquals(1, TransactionContext.getPrivateMetadata().size()); //only TransactionId + Assert.assertEquals(2, TransactionContext.getPrivateMetadata().size()); //only TransactionId and ThreadId Assert.assertEquals(0, listener.events.size()); } @@ -67,9 +67,9 @@ public void testDestroyWithNoCreate() { public void testSingleCreateSingleDestroy() { TransactionContext.create(); Assert.assertEquals(1, TransactionContext.getReferenceCounter().get()); - Assert.assertEquals(2, TransactionContext.getPrivateMetadata().size()); //only TransactionId and Ref Counter + Assert.assertEquals(3, TransactionContext.getPrivateMetadata().size()); //only TransactionId, ThreadId and Ref Counter TransactionContext.putMetadata("foo", "bar"); - Assert.assertEquals(3, TransactionContext.getPrivateMetadata().size()); //only TransactionId and Ref Counter and Foobar + Assert.assertEquals(4, TransactionContext.getPrivateMetadata().size()); //only TransactionId, ThreadId, Ref Counter and Foobar TransactionContext.destroy(); testInitialValue(); Assert.assertEquals(2, listener.events.size()); @@ -81,9 +81,9 @@ public void testSingleCreateSingleDestroy() { public void testSingleCreateMultipleDestroy() { TransactionContext.create(); Assert.assertEquals(1, TransactionContext.getReferenceCounter().get()); - Assert.assertEquals(2, TransactionContext.getPrivateMetadata().size()); //only TransactionId and Ref Counter + Assert.assertEquals(3, TransactionContext.getPrivateMetadata().size()); //only TransactionId, ThreadId and Ref Counter TransactionContext.putMetadata("foo", "bar"); - Assert.assertEquals(3, TransactionContext.getPrivateMetadata().size()); //only TransactionId and Ref Counter and Foobar + Assert.assertEquals(4, TransactionContext.getPrivateMetadata().size()); //only TransactionId, ThreadId, Ref Counter and Foobar // Excessive destroy calls should still remain as destroyed. TransactionContext.destroy(); @@ -99,7 +99,7 @@ public void testSingleCreateMultipleDestroy() { public void testMultipleCreateMultipleDestroy() { // Create to represent 3 layers and then destroy 3 should represent clearing. TransactionContext.create(); - Assert.assertEquals(2, TransactionContext.getPrivateMetadata().size()); + Assert.assertEquals(3, TransactionContext.getPrivateMetadata().size()); //only TransactionId, ThreadId and Ref Counter Assert.assertEquals(1, TransactionContext.getReferenceCounter().get()); TransactionContext.create(); TransactionContext.create(); @@ -118,7 +118,7 @@ public void testCreate() { TransactionContext.putMetadata("foo", "bar"); TransactionContext.create(); Assert.assertNotEquals(TransactionContext.UNINITIALIZED_TRANSACTION_CONTEXT_VALUE, TransactionContext.get()); - Assert.assertEquals(2, TransactionContext.getPrivateMetadata().size()); //only TransactionId in map and ref counter + Assert.assertEquals(3, TransactionContext.getPrivateMetadata().size()); //only TransactionId, ThreadId and Ref Counter } @Test diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedCallableTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedCallableTests.java index c8524b6..1e01093 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedCallableTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedCallableTests.java @@ -71,7 +71,7 @@ public void testNullDecoration() { public void testCall() throws Exception { Callable c = Mockito.mock(Callable.class); DecoratedCallable d = (DecoratedCallable)DecoratedCallable.maybeCreate(c); - d.parentThreadId = -1L; + d.ancestralThreadId = -1L; d.call(); Assert.assertNotNull(testListener.threadEnter); Assert.assertNotNull(testListener.threadExit); @@ -81,7 +81,7 @@ public void testCall() throws Exception { public void testCallWhenThrowsException() { Callable c = ()->{throw new RuntimeException();}; DecoratedCallable d = (DecoratedCallable)DecoratedCallable.maybeCreate(c); - d.parentThreadId = -1L; + d.ancestralThreadId = -1L; Throwable thrown = null; try { d.call(); @@ -97,7 +97,7 @@ public void testCallWhenThrowsException() { public void testCallWhenThrowsError() { Callable c = ()->{throw new Error();}; DecoratedCallable d = (DecoratedCallable)DecoratedCallable.maybeCreate(c); - d.parentThreadId = -1L; + d.ancestralThreadId = -1L; Throwable thrown = null; try { d.call(); diff --git a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedTests.java b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedTests.java index 57b4c8f..886efb2 100644 --- a/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedTests.java +++ b/disco-java-agent/disco-java-agent-core/src/test/java/software/amazon/disco/agent/concurrent/decorate/DecoratedTests.java @@ -60,7 +60,7 @@ public void testAfterSameThread() { @Test public void testBeforeDifferentThread() { Decorated d = new MyDecorated(); - d.parentThreadId = -1L; + d.ancestralThreadId = -1L; d.before(); Assert.assertTrue(listener.enter instanceof ThreadEnterEvent); Assert.assertNull(listener.exit); @@ -69,7 +69,7 @@ public void testBeforeDifferentThread() { @Test public void testAfterDifferentThread() { Decorated d = new MyDecorated(); - d.parentThreadId = -1L; + d.ancestralThreadId = -1L; d.after(); Assert.assertNull(listener.enter); Assert.assertTrue(listener.exit instanceof ThreadExitEvent); From d2ebfbdbe8c4df04c67974f700ca8becb58cbaa2 Mon Sep 17 00:00:00 2001 From: William Armiros Date: Fri, 14 Aug 2020 15:47:10 -0700 Subject: [PATCH 41/45] added HeaderReplaceable interface for ServiceDownstreamRequestEvents --- ...pacheHttpServiceDownstreamRequestEvent.java | 3 ++- .../apache/event/ApacheEventFactoryTests.java | 4 +++- .../ApacheHttpClientInterceptorTests.java | 3 ++- .../disco/agent/event/HeaderReplaceable.java | 18 ++++++++++++++++++ .../HttpServiceDownstreamRequestEvent.java | 5 ++++- 5 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HeaderReplaceable.java diff --git a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/event/ApacheHttpServiceDownstreamRequestEvent.java b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/event/ApacheHttpServiceDownstreamRequestEvent.java index f513858..890d97e 100644 --- a/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/event/ApacheHttpServiceDownstreamRequestEvent.java +++ b/disco-java-agent-web/src/main/java/software/amazon/disco/agent/web/apache/event/ApacheHttpServiceDownstreamRequestEvent.java @@ -15,13 +15,14 @@ package software.amazon.disco.agent.web.apache.event; +import software.amazon.disco.agent.event.HeaderReplaceable; import software.amazon.disco.agent.event.HttpServiceDownstreamRequestEvent; import software.amazon.disco.agent.web.apache.utils.HttpRequestAccessor; /** * Specialization allowing header replacement. */ -class ApacheHttpServiceDownstreamRequestEvent extends HttpServiceDownstreamRequestEvent { +class ApacheHttpServiceDownstreamRequestEvent extends HttpServiceDownstreamRequestEvent implements HeaderReplaceable { private final HttpRequestAccessor accessor; /** * Construct a new ApacheHttpServiceDownstreamRequestEvent diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java index c85d8ce..85be8d0 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/event/ApacheEventFactoryTests.java @@ -6,6 +6,7 @@ import org.junit.Test; import software.amazon.disco.agent.concurrent.TransactionContext; import software.amazon.disco.agent.event.EventBus; +import software.amazon.disco.agent.event.HeaderReplaceable; import software.amazon.disco.agent.event.HttpServiceDownstreamRequestEvent; import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; import software.amazon.disco.agent.web.apache.source.MockEventBusListener; @@ -48,7 +49,8 @@ public void testForRequestEventCreationForRequest() { accessor); ApacheClientTestUtil.verifyServiceRequestEvent(event); assertFalse(accessor.getHeaders().containsKey("TEST")); - event.replaceHeader("TEST","TEST"); + HeaderReplaceable replaceable = (HeaderReplaceable) event; + replaceable.replaceHeader("TEST","TEST"); assertTrue(accessor.getHeaders().containsKey("TEST")); } diff --git a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java index bcd9847..f0331b4 100644 --- a/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java +++ b/disco-java-agent-web/src/test/java/software/amazon/disco/agent/web/apache/httpclient/ApacheHttpClientInterceptorTests.java @@ -21,6 +21,7 @@ import software.amazon.disco.agent.concurrent.TransactionContext; import software.amazon.disco.agent.event.Event; import software.amazon.disco.agent.event.EventBus; +import software.amazon.disco.agent.event.HeaderReplaceable; import software.amazon.disco.agent.event.HttpServiceDownstreamRequestEvent; import software.amazon.disco.agent.event.HttpServiceDownstreamResponseEvent; import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; @@ -157,7 +158,7 @@ public void testHeaderReplacement() throws Throwable { ApacheHttpClientInterceptor.intercept(new Object[] {get}, "origin", () -> someHttpClient.execute(get)); List events = mockEventBusListener.getReceivedEvents(); - HttpServiceDownstreamRequestEvent event = (HttpServiceDownstreamRequestEvent)events.get(0); + HeaderReplaceable event = (HeaderReplaceable)events.get(0); event.replaceHeader("foo", "bar3"); assertEquals(1, get.getHeaders("foo").length); diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HeaderReplaceable.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HeaderReplaceable.java new file mode 100644 index 0000000..ec54d1f --- /dev/null +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HeaderReplaceable.java @@ -0,0 +1,18 @@ +package software.amazon.disco.agent.event; + +/** + * A composable interface that Disco Events can implement if consumers of those events should be able to manipulate + * the headers of the request associated with the event. + */ +public interface HeaderReplaceable { + + /** + * Creates the provided header if it does not exist yet, or replaces the header if it does already exist. + * This should be overridden in a concrete Event class if the functionality is available. + * + * @param key - key of the header to create or replace + * @param value - value of the header + * @return true if the header is successfully replaced + */ + boolean replaceHeader(String key, String value); +} diff --git a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HttpServiceDownstreamRequestEvent.java b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HttpServiceDownstreamRequestEvent.java index d827d4b..7e42594 100644 --- a/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HttpServiceDownstreamRequestEvent.java +++ b/disco-java-agent/disco-java-agent-api/src/main/java/software/amazon/disco/agent/event/HttpServiceDownstreamRequestEvent.java @@ -81,7 +81,10 @@ public String getUri() { } /** - * Override this method if you are a publisher of HttpServiceDownstreamRequestEvents which allows header replacement + * This method is deprecated. If you are authoring a Disco Event and would like to override this method, + * implement {@link HeaderReplaceable} instead. If you are invoking replaceHeader, you should invoke it from its + * implementing event class or by casting to {@link HeaderReplaceable} instead of invoking it from this class. + * * @param name the header name * @param value the header value * @return true if successful From 4770f1e3c945eecf5b7ff688e9ad8781faede838 Mon Sep 17 00:00:00 2001 From: Hongbo Liu Date: Mon, 17 Aug 2020 20:20:30 -0400 Subject: [PATCH 42/45] Refactored preprocess lib to explicitly invokes transform of ClassFileTransformers on all candidate classes --- .../build.gradle.kts | 51 +++++ .../instrumentation/preprocess/JarUtils.java | 66 ++++++ .../ThreadSubclassInterceptorIntegTest.java | 108 ++++++++++ .../preprocess/mocks/IntegTestListener.java | 48 +++++ .../preprocess/mocks/IntegTestThread.java | 26 +++ .../preprocess/cli/Driver.java | 68 ++++-- .../preprocess/cli/PreprocessConfig.java | 8 +- .../cli/PreprocessConfigParser.java | 46 +---- .../AgentLoaderNotProvidedException.java | 4 +- .../exceptions/ArgumentParserException.java | 32 +++ ... ClassFileLoaderNotProvidedException.java} | 12 +- ...ortException.java => ExportException.java} | 6 +- .../exceptions/InstrumentationException.java | 33 +++ .../exceptions/JarEntryReadException.java | 35 ++++ .../UnableToReadJarEntryException.java | 20 -- ...xportStrategy.java => ExportStrategy.java} | 9 +- ...rtStrategy.java => JarExportStrategy.java} | 106 +++++----- .../instrumentation/ModuleTransformer.java | 118 ----------- .../StaticInstrumentationTransformer.java | 127 ++++++++++++ .../TransformationListener.java | 21 +- .../loaders/agents/DiscoAgentLoader.java | 17 +- .../loaders/agents/TransformerExtractor.java | 75 +++++++ .../ClassFileLoader.java} | 14 +- .../JarInfo.java} | 16 +- .../loaders/classfiles/JarLoader.java | 139 +++++++++++++ .../loaders/modules/JarModuleLoader.java | 192 ----------------- .../preprocess/util/JarFileUtils.java | 53 +++++ .../preprocess/util/PreprocessConstants.java | 15 ++ .../instrumentation/preprocess/JarUtils.java | 41 ++++ .../preprocess/{util => }/MockEntities.java | 34 ++- .../cli/PreprocessConfigParserTest.java | 60 +++--- ...gyTest.java => JarExportStrategyTest.java} | 81 ++++---- ...StaticInstrumentationTransformerTest.java} | 103 ++++++---- .../TransformationListenerTest.java | 8 +- .../loaders/agents/DiscoAgentLoaderTest.java | 49 ++--- .../agents/TransformerExtractorTest.java | 44 ++++ .../loaders/classfiles/JarLoaderTest.java | 108 ++++++++++ .../loaders/modules/JarModuleLoaderTest.java | 193 ------------------ .../preprocess/util/JarfileUtilsTest.java | 60 ++++++ 39 files changed, 1397 insertions(+), 849 deletions(-) create mode 100644 disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/JarUtils.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/ThreadSubclassInterceptorIntegTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/mocks/IntegTestListener.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/mocks/IntegTestThread.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ArgumentParserException.java rename disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/{ModuleLoaderNotProvidedException.java => ClassFileLoaderNotProvidedException.java} (77%) rename disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/{ModuleExportException.java => ExportException.java} (87%) create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InstrumentationException.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/JarEntryReadException.java delete mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToReadJarEntryException.java rename disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/{ModuleExportStrategy.java => ExportStrategy.java} (76%) rename disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/{JarModuleExportStrategy.java => JarExportStrategy.java} (65%) delete mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformer.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/StaticInstrumentationTransformer.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java rename disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/{modules/ModuleLoader.java => classfiles/ClassFileLoader.java} (64%) rename disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/{modules/ModuleInfo.java => classfiles/JarInfo.java} (72%) create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarLoader.java delete mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/JarUtils.java rename disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/{util => }/MockEntities.java (82%) rename disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/{JarModuleExportStrategyTest.java => JarExportStrategyTest.java} (71%) rename disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/{ModuleTransformerTest.java => StaticInstrumentationTransformerTest.java} (53%) create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarLoaderTest.java delete mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java create mode 100644 disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarfileUtilsTest.java diff --git a/disco-java-agent-instrumentation-preprocess/build.gradle.kts b/disco-java-agent-instrumentation-preprocess/build.gradle.kts index 8c0a604..7a8d0dc 100644 --- a/disco-java-agent-instrumentation-preprocess/build.gradle.kts +++ b/disco-java-agent-instrumentation-preprocess/build.gradle.kts @@ -25,4 +25,55 @@ dependencies { implementation("org.apache.logging.log4j", "log4j-core", "2.13.3") testImplementation(project(":disco-java-agent:disco-java-agent-core")) testImplementation(project(":disco-java-agent:disco-java-agent-api")) +} + +/** + * Define a secondary set of tests, for testing the actual interceptions provided by the Installables. + */ +sourceSets { + create("integtest") { + java { + srcDir("src/integtest/java") + } + } +} + +//create a new empty integ test config - not extending from existing compile or testCompile, since we don't want to +//be able to compile against Core etc. +val integtestImplementation: Configuration by configurations.getting {} + +dependencies { + integtestImplementation("junit", "junit", "4.12") + integtestImplementation("net.bytebuddy", "byte-buddy-dep", "1.9.12") + integtestImplementation("org.ow2.asm", "asm", "7.1") + integtestImplementation("org.apache.logging.log4j", "log4j-core", "2.13.3") + integtestImplementation(project(":disco-java-agent:disco-java-agent-api")) + integtestImplementation(project(":disco-java-agent:disco-java-agent-inject-api", "shadow")) + integtestImplementation(project(":disco-java-agent-instrumentation-preprocess", "shadow")) +} + +val ver = project.version + +val integtest = task("integtest") { + testClassesDirs = sourceSets["integtest"].output.classesDirs + + classpath = sourceSets["integtest"].runtimeClasspath + .minus(configurations.compileClasspath.get()) + .filter { + // need to remove disco agent api from classpath because the agent to be loaded already has it as dependency + file -> !file.endsWith("disco-java-agent-api-"+ver+".jar") + } + .plus(sourceSets["integtest"].runtimeClasspath.filter { + // add back bytebuddy and asm dependencies to the classpath + file -> file.absolutePath.contains("net.bytebuddy") || file.absolutePath.contains("org.ow2.asm") + } + ) + + //we need the agent to be built first + dependsOn(":disco-java-agent:disco-java-agent:build") + mustRunAfter(tasks["test"]) +} + +tasks.build { + dependsOn(integtest) } \ No newline at end of file diff --git a/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/JarUtils.java b/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/JarUtils.java new file mode 100644 index 0000000..fc39f5f --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/JarUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess; + +import net.bytebuddy.ByteBuddy; +import org.junit.rules.TemporaryFolder; +import software.amazon.disco.agent.event.Listener; +import software.amazon.disco.agent.reflect.event.EventBus; + +import java.io.File; +import java.io.FileOutputStream; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +public class JarUtils { + public static File createJar(TemporaryFolder temporaryFolder, String fileName, Map entries) throws Exception { + File file = temporaryFolder.newFile(fileName + ".jar"); + + try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + for (Map.Entry entry : entries.entrySet()) { + jarOutputStream.putNextEntry(new ZipEntry(entry.getKey())); + jarOutputStream.write(entry.getValue()); + jarOutputStream.closeEntry(); + } + } + } + return file; + } + + public static File makeTargetJarWithRenamedClasses(List types, String suffix, TemporaryFolder temporaryFolder) throws Exception { + /** + * must rename all classes to be statically instrumented in order to invoke the transformed version of these classes. + * otherwise the JVM will always resolve to the original .class files when instantiating them using the original fully qualified name. + */ + Map entries = new HashMap<>(); + for (Class clazz : types) { + byte[] bytes = new ByteBuddy() + .redefine(clazz) + .name(clazz.getName() + suffix) + .make() + .getBytes(); + entries.put(clazz.getName().replace('.', '/') + suffix + ".class", bytes); + } + + return createJar(temporaryFolder, "IntegJarTarget", entries); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/ThreadSubclassInterceptorIntegTest.java b/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/ThreadSubclassInterceptorIntegTest.java new file mode 100644 index 0000000..d0ce64c --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/ThreadSubclassInterceptorIntegTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess; + +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import software.amazon.disco.agent.event.ThreadEnterEvent; +import software.amazon.disco.agent.event.ThreadExitEvent; +import software.amazon.disco.agent.inject.Injector; +import software.amazon.disco.agent.reflect.concurrent.TransactionContext; +import software.amazon.disco.instrumentation.preprocess.cli.Driver; +import software.amazon.disco.instrumentation.preprocess.mocks.IntegTestListener; +import software.amazon.disco.instrumentation.preprocess.mocks.IntegTestThread; + +import java.io.File; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * This test class statically instruments a jarfile containing only one bare bone class named IntegTestThread that + * extends Thread using an pluggable disco agent with concurrency support as default. + *

+ * The goal of this integ test is to check whether ThreadEnterEvent and ThreadExitEvent events are published and captured + * by the TestListener when invoking {@link Thread#start()} of a transformed IntegTestThread instance. + */ +public class ThreadSubclassInterceptorIntegTest { + private static final String RENAME_SUFFIX = "Redefined"; + + @ClassRule + public static TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void beforeClass() throws Exception { + String srcJarPath = JarUtils.makeTargetJarWithRenamedClasses(Arrays.asList(IntegTestThread.class), "Redefined", temporaryFolder).getAbsolutePath(); + String arg = "verbose:loggerfactory=software.amazon.disco.agent.reflect.logging.StandardOutputLoggerFactory"; + + File agentPath = new File("../disco-java-agent/disco-java-agent/build/libs/") + .listFiles((dir1, name) -> name.startsWith("disco-java-agent-") && name.endsWith(".jar"))[0]; + + String[] commandAndArgs = new String[]{ + "-jps", srcJarPath, + "-ap", agentPath.getAbsolutePath(), + "-out", temporaryFolder.getRoot().getAbsolutePath(), + "-arg", arg, + "--verbose" + }; + + Driver.main(commandAndArgs); + + Injector.addToSystemClasspath(Injector.createInstrumentation(), new File(srcJarPath)); + } + + @Test + public void testStaticInstrumentationOnThreadSubclassInterceptorWorks() throws Exception { + TransactionContext.create(); + IntegTestListener listener = new IntegTestListener(); + LinkedHashMap eventsRegistry = listener.getEventsRegistry(); + listener.register(); + + IntegTestThread original = new IntegTestThread(); + original.start(); + original.join(); + + Assert.assertTrue(eventsRegistry.isEmpty()); + + Thread transformed = (Thread) Class.forName(IntegTestThread.class.getName() + RENAME_SUFFIX) + .getDeclaredConstructor() + .newInstance(); + + transformed.start(); + transformed.join(); + + Assert.assertEquals(2, eventsRegistry.size()); + Assert.assertTrue(eventsRegistry.containsKey(ThreadEnterEvent.class)); + Assert.assertTrue(eventsRegistry.containsKey(ThreadExitEvent.class)); + Assert.assertEquals(1, eventsRegistry.get(ThreadEnterEvent.class).intValue()); + Assert.assertEquals(1, eventsRegistry.get(ThreadExitEvent.class).intValue()); + + // Iterate through the ordered LinkedHashMap to verify if ThreadExitEvent is published after ThreadEnterEvent + boolean enterEventFound = false; + for(Class clazz : eventsRegistry.keySet()){ + if(clazz.equals(ThreadEnterEvent.class)){ + enterEventFound = true; + }else if(clazz.equals(ThreadExitEvent.class)){ + // test will fail if ThreadEnterEvent wasn't published before ThreadExitEvent + Assert.assertTrue(enterEventFound); + break; + } + } + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/mocks/IntegTestListener.java b/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/mocks/IntegTestListener.java new file mode 100644 index 0000000..37f7d4f --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/mocks/IntegTestListener.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.mocks; + +import software.amazon.disco.agent.event.Event; +import software.amazon.disco.agent.event.Listener; +import software.amazon.disco.agent.reflect.event.EventBus; + +import java.util.LinkedHashMap; + +public class IntegTestListener implements Listener { + private LinkedHashMap eventsRegistry; + + public IntegTestListener() { + eventsRegistry = new LinkedHashMap<>(); + } + + @Override + public int getPriority() { + return 0; + } + + @Override + public void listen(Event e) { + eventsRegistry.put(e.getClass(), eventsRegistry.getOrDefault(e.getClass(), 0) + 1); + } + + public LinkedHashMap getEventsRegistry(){ + return eventsRegistry; + } + + public void register(){ + EventBus.addListener(this); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/mocks/IntegTestThread.java b/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/mocks/IntegTestThread.java new file mode 100644 index 0000000..e36fcd8 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/integtest/java/software/amazon/disco/instrumentation/preprocess/mocks/IntegTestThread.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.mocks; + +/** + * This class will be used to test static instrumentation using a disco agent with concurrency support. + * + * When constructing a jar file to be transformed, this class must be renamed using new ByteBuddy().redefine().rename() in + * order to be able to instantiate the instrumented version otherwise the JVM will always resolve to this original class even if the + * instrumented version is appended to the class path. + */ +public class IntegTestThread extends Thread { +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java index d0c4f09..e2f778e 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/Driver.java @@ -15,37 +15,67 @@ package software.amazon.disco.instrumentation.preprocess.cli; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import software.amazon.disco.agent.inject.Injector; -import software.amazon.disco.instrumentation.preprocess.instrumentation.ModuleTransformer; +import software.amazon.disco.instrumentation.preprocess.instrumentation.StaticInstrumentationTransformer; import software.amazon.disco.instrumentation.preprocess.loaders.agents.DiscoAgentLoader; -import software.amazon.disco.instrumentation.preprocess.loaders.modules.JarModuleLoader; +import software.amazon.disco.instrumentation.preprocess.loaders.classfiles.JarLoader; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; import java.io.File; -import java.lang.instrument.Instrumentation; /** - * Entry point of the library. A {@link ModuleTransformer} instance is being created to orchestrate the instrumentation - * process of all packages supplied. + * Entry point of the library. A {@link StaticInstrumentationTransformer} instance is being created to orchestrate the instrumentation + * process of all class files supplied. */ public class Driver { - public static void main(String[] args) { - final PreprocessConfig config = new PreprocessConfigParser().parseCommandLine(args); + private static final Logger log = LogManager.getLogger(Driver.class); - if (config == null) { - System.exit(1); + public static void main(String[] args) { + // only print help text if it's the first argument passed in and ignores all other args + if (args[0].toLowerCase().equals("--help")) { + printHelpText(); + System.exit(0); } - final Instrumentation instrumentation = Injector.createInstrumentation(); + try { + final PreprocessConfig config = new PreprocessConfigParser().parseCommandLine(args); + + // inject the agent jar into the classpath as earlier as possible to avoid ClassNotFound exception when resolving + // types imported from libraries such as ByteBuddy shaded in the agent JAR + Injector.addToBootstrapClasspath(Injector.createInstrumentation(), new File(config.getAgentPath())); - // inject the agent jar into the classpath as earlier as possible to avoid ClassNotFound exception when resolving - // types imported from libraries such as ByteBuddy shaded in the agent JAR - Injector.addToBootstrapClasspath(instrumentation, new File(config.getAgentPath())); + StaticInstrumentationTransformer.builder() + .agentLoader(new DiscoAgentLoader()) + .jarLoader(new JarLoader()) + .config(config) + .build() + .transform(); + } catch (RuntimeException e) { + log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to perform static instrumentation", e); + System.exit(1); + } + } - ModuleTransformer.builder() - .agentLoader(new DiscoAgentLoader()) - .jarLoader(new JarModuleLoader()) - .config(config) - .build() - .transform(); + /** + * Prints out the help text when the [--help] option is passed. + */ + protected static void printHelpText() { + System.out.println("Disco Instrumentation Preprocess Library Command Line Interface\n" + + "\t Usage: [options] \n" + + "\t\t --help List all supported options supported by the CLI.\n" + + "\t\t --outputDir | -out \n" + + "\t\t --jarPaths | -jps \n" + + "\t\t --serializationPath | -sp \n" + + "\t\t --agentPath | -ap \n" + + "\t\t --agentArg | -arg \n" + + "\t\t --suffix | -suf \n" + + "\t\t --javaversion | -jv \n" + + "\t\t --verbose Set the log level to log everything.\n" + + "\t\t --silent Disable logging to the console.\n\n" + + "The default behavior of the library will replace the original Jar scheduled for instrumentation if NO outputDir AND suffix are supplied.\n" + + "One agentPath and at least one jarPaths must be provided in order to perform static instrumentation" + ); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java index bc9037b..009ba87 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfig.java @@ -38,6 +38,12 @@ public class PreprocessConfig { private final Level logLevel; private final String serializationJarPath; private final String javaVersion; + private final String agentArg; - private final AgentConfig coreAgentConfig; + public Level getLogLevel() { + if(logLevel == null){ + return Level.INFO; + } + return logLevel; + } } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java index d6820c2..2129059 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParser.java @@ -19,7 +19,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.Level; -import software.amazon.disco.agent.config.AgentConfig; +import software.amazon.disco.instrumentation.preprocess.exceptions.ArgumentParserException; import java.util.HashMap; import java.util.Map; @@ -39,14 +39,7 @@ public class PreprocessConfigParser { */ public PreprocessConfig parseCommandLine(String[] args) { if (args == null || args.length == 0) { - System.err.println("Mandatory options not supplied, please use [--help] to get a list of all options supported by this CLI."); - return null; - } - - // only print help text if it's the first argument passed in and ignores all other args - if (args[0].toLowerCase().equals("--help")) { - printHelpText(); - return null; + throw new ArgumentParserException("Mandatory options not supplied, please use [--help] to get a list of all options supported by this CLI."); } setupAcceptedFlags(); @@ -61,8 +54,7 @@ public PreprocessConfig parseCommandLine(String[] args) { // no previous flag found, expecting a flag if (!ACCEPTED_FLAGS.containsKey(argLowered)) { - System.err.println("Flag: [" + arg + "] is invalid"); - return null; + throw new ArgumentParserException("Flag: [" + arg + "] is invalid"); } final OptionToMatch option = ACCEPTED_FLAGS.get(argLowered); @@ -74,8 +66,7 @@ public PreprocessConfig parseCommandLine(String[] args) { } else { // previous flag still expecting an argument but another flag is discovered if (ACCEPTED_FLAGS.containsKey(argLowered) && !flagBeingMatched.isMatched()) { - System.err.println("Flag: [" + flagBeingMatched.getFlag() + "] requires an argument"); - return null; + throw new ArgumentParserException("Flag: [" + flagBeingMatched.getFlag() + "] requires an argument"); } // a previously detected option that accepts multi values is now finished matching its arguments. @@ -99,8 +90,7 @@ public PreprocessConfig parseCommandLine(String[] args) { // the last flag discovered is missing its arg if (flagBeingMatched != null && !flagBeingMatched.isMatched) { - System.err.println("Flag: [" + flagBeingMatched.getFlag() + "] requires an argument"); - return null; + throw new ArgumentParserException("Flag: [" + flagBeingMatched.getFlag() + "] requires an argument"); } return builder.build(); @@ -120,6 +110,7 @@ protected void setupAcceptedFlags() { ACCEPTED_FLAGS.put("--serializationpath", new OptionToMatch("--serializationpath", true, false)); ACCEPTED_FLAGS.put("--suffix", new OptionToMatch("--suffix", true, false)); ACCEPTED_FLAGS.put("--javaversion", new OptionToMatch("--javaversion", true, false)); + ACCEPTED_FLAGS.put("--agentarg", new OptionToMatch("--agentarg", true, false)); ACCEPTED_FLAGS.put("-out", new OptionToMatch("-out", true, false)); ACCEPTED_FLAGS.put("-jps", new OptionToMatch("-jps", true, true)); @@ -127,26 +118,7 @@ protected void setupAcceptedFlags() { ACCEPTED_FLAGS.put("-sp", new OptionToMatch("-sp", true, false)); ACCEPTED_FLAGS.put("-suf", new OptionToMatch("-suf", true, false)); ACCEPTED_FLAGS.put("-jv", new OptionToMatch("-jv", true, false)); - } - - /** - * Prints out the help text when the [--help] option is passed. - */ - protected void printHelpText() { - System.out.println("Disco Instrumentation Preprocess Library Command Line Interface\n" - + "\t Usage: [options] \n" - + "\t\t --help List all supported options supported by the CLI.\n" - + "\t\t --outputDir | -out \n" - + "\t\t --jarPaths | -jps \n" - + "\t\t --serializationPath | -sp \n" - + "\t\t --agentPath | -ap \n" - + "\t\t --suffix | -suf \n" - + "\t\t --javaversion | -jv \n" - + "\t\t --verbose Set the log level to log everything.\n" - + "\t\t --silent Disable logging to the console.\n\n" - + "The default behavior of the library will replace the original package scheduled for instrumentation if NO destination AND suffix are supplied.\n" - + "An agent AND either a servicePackage or at least one dependenciesPath MUST be supplied." - ); + ACCEPTED_FLAGS.put("-arg", new OptionToMatch("-arg", true, false)); } /** @@ -179,6 +151,10 @@ protected OptionToMatch matchArgWithFlag(OptionToMatch option, String argument, case "--agentpath": builder.agentPath(argument); break; + case "-arg": + case "--agentarg": + builder.agentArg(argument); + break; case "-suf": case "--suffix": builder.suffix(argument); diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/AgentLoaderNotProvidedException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/AgentLoaderNotProvidedException.java index e42bc73..b8bf47f 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/AgentLoaderNotProvidedException.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/AgentLoaderNotProvidedException.java @@ -15,13 +15,13 @@ package software.amazon.disco.instrumentation.preprocess.exceptions; -import software.amazon.disco.instrumentation.preprocess.instrumentation.ModuleTransformer; +import software.amazon.disco.instrumentation.preprocess.instrumentation.StaticInstrumentationTransformer; import software.amazon.disco.instrumentation.preprocess.loaders.agents.AgentLoader; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; /** * Exception thrown when a valid {@link AgentLoader} is not provided to - * {@link ModuleTransformer} + * {@link StaticInstrumentationTransformer} */ public class AgentLoaderNotProvidedException extends RuntimeException { /** diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ArgumentParserException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ArgumentParserException.java new file mode 100644 index 0000000..f813fd5 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ArgumentParserException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown while parsing arguments received via the CLI + */ +public class ArgumentParserException extends RuntimeException{ + /** + * Constructor accepting a String argument that describes the exception cause + * + * @param message message that describes the cause + */ + public ArgumentParserException(String message) { + super(PreprocessConstants.MESSAGE_PREFIX+ message); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleLoaderNotProvidedException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ClassFileLoaderNotProvidedException.java similarity index 77% rename from disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleLoaderNotProvidedException.java rename to disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ClassFileLoaderNotProvidedException.java index 2c0f9f9..c64f403 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleLoaderNotProvidedException.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ClassFileLoaderNotProvidedException.java @@ -15,19 +15,19 @@ package software.amazon.disco.instrumentation.preprocess.exceptions; -import software.amazon.disco.instrumentation.preprocess.instrumentation.ModuleTransformer; -import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleLoader; +import software.amazon.disco.instrumentation.preprocess.instrumentation.StaticInstrumentationTransformer; +import software.amazon.disco.instrumentation.preprocess.loaders.classfiles.ClassFileLoader; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; /** - * Exception thrown when a valid {@link ModuleLoader} is not provided to - * {@link ModuleTransformer} + * Exception thrown when a valid {@link ClassFileLoader} is not provided to + * {@link StaticInstrumentationTransformer} */ -public class ModuleLoaderNotProvidedException extends RuntimeException { +public class ClassFileLoaderNotProvidedException extends RuntimeException { /** * Constructor invoking the parent constructor with a fixed error message */ - public ModuleLoaderNotProvidedException() { + public ClassFileLoaderNotProvidedException() { super(PreprocessConstants.MESSAGE_PREFIX + "package loader not provided"); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleExportException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ExportException.java similarity index 87% rename from disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleExportException.java rename to disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ExportException.java index d6f0bf8..e745a25 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ModuleExportException.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/ExportException.java @@ -20,15 +20,15 @@ /** * Exception thrown when an error has occurred during the export process */ -public class ModuleExportException extends RuntimeException { +public class ExportException extends RuntimeException { /** - * Constructor that accepts a message explaining why the module export process failed as well as + * Constructor that accepts a message explaining why the module export process failed and * a {@link Throwable} instance for tracing. * * @param message cause of the failure * @param cause {@link Throwable cause} of the failure for tracing the root cause. */ - public ModuleExportException(String message, Throwable cause) { + public ExportException(String message, Throwable cause) { super(PreprocessConstants.MESSAGE_PREFIX + message, cause); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InstrumentationException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InstrumentationException.java new file mode 100644 index 0000000..cdbbe43 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/InstrumentationException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when the library fails to instrument a target class. + */ +public class InstrumentationException extends RuntimeException { + /** + * Constructor accepting a message explaining the error as well as a {@link Throwable cause} + * + * @param message detailed message explaining the failure + * @param cause cause of the exception + */ + public InstrumentationException(String message, Throwable cause) { + super(PreprocessConstants.MESSAGE_PREFIX + message, cause); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/JarEntryReadException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/JarEntryReadException.java new file mode 100644 index 0000000..a8d8868 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/JarEntryReadException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.exceptions; + +import software.amazon.disco.instrumentation.preprocess.export.ExportStrategy; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +/** + * Exception thrown when the {@link ExportStrategy exporter} + * fails to read an exiting entry from the original Jar file. + */ +public class JarEntryReadException extends RuntimeException { + /** + * Constructor + * + * @param entryName {@link java.util.jar.JarEntry} that failed to be copied + * @param cause {@link Throwable cause} of the failure for tracing the root cause. + */ + public JarEntryReadException(String entryName, Throwable cause) { + super(PreprocessConstants.MESSAGE_PREFIX + "Failed to read Jar entry: " + entryName, cause); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToReadJarEntryException.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToReadJarEntryException.java deleted file mode 100644 index 8bcc473..0000000 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/exceptions/UnableToReadJarEntryException.java +++ /dev/null @@ -1,20 +0,0 @@ -package software.amazon.disco.instrumentation.preprocess.exceptions; - -import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; -import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; - -/** - * Exception thrown when the {@link ModuleExportStrategy exporter} - * fails to read an exiting entry from the original Jar file. - */ -public class UnableToReadJarEntryException extends RuntimeException { - /** - * Constructor - * - * @param entryName {@link java.util.jar.JarEntry} that failed to be copied - * @param cause {@link Throwable cause} of the failure for tracing the root cause. - */ - public UnableToReadJarEntryException(String entryName, Throwable cause) { - super(PreprocessConstants.MESSAGE_PREFIX + "Failed to read Jar entry: " + entryName, cause); - } -} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ExportStrategy.java similarity index 76% rename from disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java rename to disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ExportStrategy.java index b2e0b78..6200ebe 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ModuleExportStrategy.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/ExportStrategy.java @@ -15,21 +15,22 @@ package software.amazon.disco.instrumentation.preprocess.export; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; -import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; +import software.amazon.disco.instrumentation.preprocess.loaders.classfiles.JarInfo; import java.util.Map; /** * Interface for the strategy to use when exporting transformed classes */ -public interface ModuleExportStrategy { +public interface ExportStrategy { /** * Strategy called to export all transformed classes. * * @param info Information of the original Jar * @param instrumented a map of instrumented classes with their bytecode - * @param suffix suffix to be appended to the transformed package + * @param config configuration file containing instructions to instrument a module */ - void export(final ModuleInfo info, final Map instrumented, final String suffix); + void export(final JarInfo info, final Map instrumented, final PreprocessConfig config); } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarExportStrategy.java similarity index 65% rename from disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java rename to disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarExportStrategy.java index ebce64e..e3d4166 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategy.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/export/JarExportStrategy.java @@ -17,10 +17,11 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleExportException; -import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; +import software.amazon.disco.instrumentation.preprocess.exceptions.ExportException; +import software.amazon.disco.instrumentation.preprocess.exceptions.JarEntryReadException; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; -import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; +import software.amazon.disco.instrumentation.preprocess.loaders.classfiles.JarInfo; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; import java.io.File; @@ -40,45 +41,26 @@ /** * Strategy to export transformed classes to a local Jar */ -public class JarModuleExportStrategy implements ModuleExportStrategy { - private static final Logger log = LogManager.getLogger(JarModuleExportStrategy.class); +public class JarExportStrategy implements ExportStrategy { + private static final Logger log = LogManager.getLogger(JarExportStrategy.class); private static Path tempDir = null; - private final String outputDir; - - /** - * Constructor with the destination path provided. - * - * @param outputDir Absolute output path of the transformed jar. Default is the current folder where - * the original jar is located. - */ - public JarModuleExportStrategy(final String outputDir) { - this.outputDir = outputDir; - } - - /** - * Constructor with no destination path provided. Transformed jar will replace the original file. - */ - public JarModuleExportStrategy() { - this.outputDir = null; - } - /** * Exports all transformed classes to a Jar file. A temporary Jar File will be created to store all * the transformed classes and then be renamed to replace the original Jar. * - * @param moduleInfo Information of the original Jar + * @param jarInfo Information of the original Jar * @param instrumented a map of instrumented classes with their bytecode - * @param suffix suffix of the transformed package + * @param config configuration file containing instructions to instrument a module */ @Override - public void export(final ModuleInfo moduleInfo, final Map instrumented, final String suffix) { + public void export(final JarInfo jarInfo, final Map instrumented, final PreprocessConfig config) { log.debug(PreprocessConstants.MESSAGE_PREFIX + "Saving changes to Jar"); - final File file = createTempFile(moduleInfo); + final File file = createTempFile(jarInfo); - buildOutputJar(moduleInfo, instrumented, file); - moveTempFileToDestination(moduleInfo, suffix, file); + buildOutputJar(jarInfo, instrumented, file); + moveTempFileToDestination(jarInfo, config, file); } /** @@ -86,18 +68,18 @@ public void export(final ModuleInfo moduleInfo, final Map instrumented) { + log.info(PreprocessConstants.MESSAGE_PREFIX + "Transformed " + instrumented.size() + " classes."); for (Map.Entry mapEntry : instrumented.entrySet()) { final String classPath = mapEntry.getKey(); final InstrumentedClassState info = mapEntry.getValue(); @@ -120,7 +103,7 @@ protected void saveTransformedClasses(final JarOutputStream jarOS, final Map instrumented) { - for (Enumeration entries = moduleInfo.getJarFile().entries(); entries.hasMoreElements(); ) { + protected void copyExistingJarEntries(final JarOutputStream jarOS, final JarFile jarFile, final Map instrumented) { + log.info(PreprocessConstants.MESSAGE_PREFIX + "Copying existing entries from file: " + jarFile.getName()); + final Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { final JarEntry entry = (JarEntry) entries.nextElement(); final String keyToCheck = entry.getName().endsWith(".class") ? entry.getName().substring(0, entry.getName().lastIndexOf(".class")) : entry.getName(); @@ -143,11 +128,11 @@ protected void copyExistingJarEntries(final JarOutputStream jarOS, final ModuleI if (entry.isDirectory()) { jarOS.putNextEntry(entry); } else { - copyJarEntry(jarOS, moduleInfo.getJarFile(), entry); + copyJarEntry(jarOS, jarFile, entry); } } } catch (IOException e) { - throw new ModuleExportException("Failed to copy class: " + entry.getName(), e); + throw new ExportException("Failed to copy class: " + entry.getName(), e); } } } @@ -155,16 +140,16 @@ protected void copyExistingJarEntries(final JarOutputStream jarOS, final ModuleI /** * Builds the output jar by copying existing entries from the original and inserting transformed classes * - * @param moduleInfo Information of the original Jar + * @param jarInfo Information of the original Jar * @param instrumented a map of instrumented classes with their bytecode * @param tempFile file that the JarOutputStream will write to */ - protected void buildOutputJar(final ModuleInfo moduleInfo, final Map instrumented, final File tempFile) { - try (JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(tempFile))) { - copyExistingJarEntries(jarOS, moduleInfo, instrumented); + protected void buildOutputJar(final JarInfo jarInfo, final Map instrumented, final File tempFile) { + try (JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(tempFile)); JarFile jarFile = new JarFile(jarInfo.getFile())) { + copyExistingJarEntries(jarOS, jarFile, instrumented); saveTransformedClasses(jarOS, instrumented); } catch (IOException e) { - throw new ModuleExportException("Failed to create output Jar file", e); + throw new ExportException("Failed to create output Jar file", e); } } @@ -178,10 +163,11 @@ protected void buildOutputJar(final ModuleInfo moduleInfo, final Map - * At least one valid {@link AgentLoader} AND one {@link ModuleLoader} must be provided (either a service package loader - * or a dependency package loader). - */ -@Builder -@AllArgsConstructor -public class ModuleTransformer { - private static final Logger log = LogManager.getLogger(ModuleTransformer.class); - - private final ModuleLoader jarLoader; - private final AgentLoader agentLoader; - private final PreprocessConfig config; - - /** - * This method initiates the transformation process of all packages found under the provided paths. All Runtime exceptions - * thrown by the library are handled in this method. A detailed error message along with any available Cause will be logged - * and trigger the program to exit with status 1 - */ - public void transform() { - try { - if (config == null) {throw new InvalidConfigEntryException("No configuration provided", null);} - - if (config.getLogLevel() == null) { - Configurator.setRootLevel(Level.INFO); - } else { - Configurator.setRootLevel(config.getLogLevel()); - } - - if (agentLoader == null) {throw new AgentLoaderNotProvidedException();} - if (jarLoader == null) {throw new ModuleLoaderNotProvidedException();} - - agentLoader.loadAgent(config, Injector.createInstrumentation()); - - if (jarLoader == null) { - throw new ModuleLoaderNotProvidedException(); - } - - // Apply instrumentation on all jars - for (final ModuleInfo info : jarLoader.loadPackages(config)) { - applyInstrumentation(info); - //todo: store serialized instrumentation state to target jar - } - } catch (RuntimeException e) { - log.error(e); - System.exit(1); - } - } - - /** - * Triggers instrumentation of classes using Reflection and applies the changes according to the - * {@link ModuleExportStrategy export strategy} - * of this package - * - * @param moduleInfo a package containing classes to be instrumented - */ - protected void applyInstrumentation(final ModuleInfo moduleInfo) { - for (String name : moduleInfo.getClassNames()) { - try { - Class.forName(name); - } catch (ClassNotFoundException | NoClassDefFoundError e) { - log.warn(PreprocessConstants.MESSAGE_PREFIX + "Failed to initialize class:" + name, e); - } - } - - moduleInfo.getExportStrategy().export(moduleInfo, getInstrumentedClasses(), config.getSuffix()); - - // empty the map in preparation for transforming another package - getInstrumentedClasses().clear(); - } - - /** - * Fetches instrumented classes from the listener attached to all {@link software.amazon.disco.agent.interception.Installable installables}. - * - * @return a Map of class name as key and {@link InstrumentedClassState} as value - */ - protected Map getInstrumentedClasses() { - return TransformationListener.getInstrumentedTypes(); - } -} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/StaticInstrumentationTransformer.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/StaticInstrumentationTransformer.java new file mode 100644 index 0000000..75181c5 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/StaticInstrumentationTransformer.java @@ -0,0 +1,127 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.instrumentation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.config.Configurator; +import software.amazon.disco.agent.inject.Injector; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; +import software.amazon.disco.instrumentation.preprocess.exceptions.AgentLoaderNotProvidedException; +import software.amazon.disco.instrumentation.preprocess.exceptions.InstrumentationException; +import software.amazon.disco.instrumentation.preprocess.exceptions.InvalidConfigEntryException; +import software.amazon.disco.instrumentation.preprocess.exceptions.ClassFileLoaderNotProvidedException; +import software.amazon.disco.instrumentation.preprocess.export.ExportStrategy; +import software.amazon.disco.instrumentation.preprocess.loaders.agents.AgentLoader; +import software.amazon.disco.instrumentation.preprocess.loaders.agents.TransformerExtractor; +import software.amazon.disco.instrumentation.preprocess.loaders.classfiles.JarInfo; +import software.amazon.disco.instrumentation.preprocess.loaders.classfiles.ClassFileLoader; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; +import java.util.Map; + +/** + * Class responsible to orchestrate the instrumentation process involving agent loading, package loading, instrumentation + * triggering and exporting the transformed classes. + *

+ * At least one valid {@link AgentLoader} AND one {@link ClassFileLoader} must be provided (either a service package loader + * or a dependency package loader). + */ +@Builder +@AllArgsConstructor +public class StaticInstrumentationTransformer { + private static final Logger log = LogManager.getLogger(StaticInstrumentationTransformer.class); + + private final ClassFileLoader jarLoader; + private final AgentLoader agentLoader; + private final PreprocessConfig config; + + /** + * This method initiates the transformation process of all packages found under the provided paths. All Runtime exceptions + * thrown by the library are handled in this method. A detailed error message along with any available Cause will be logged + * and trigger the program to exit with status 1 + */ + public void transform() { + if (config == null) { + throw new InvalidConfigEntryException("No configuration provided", null); + } + // setting the level again here in case the lib is imported as a dependency and the arg parser is never used + Configurator.setRootLevel(config.getLogLevel()); + + if (agentLoader == null) { + throw new AgentLoaderNotProvidedException(); + } + if (jarLoader == null) { + throw new ClassFileLoaderNotProvidedException(); + } + + agentLoader.loadAgent(config, new TransformerExtractor(Injector.createInstrumentation())); + + log.info(PreprocessConstants.MESSAGE_PREFIX + TransformerExtractor.getTransformers().size() + " Installables loaded."); + + // Apply instrumentation on all loaded jars + for (final JarInfo info : jarLoader.load(config)) { + applyInstrumentation(info); + } + } + + /** + * Triggers instrumentation of classes by invoking {@link ClassFileTransformer#transform(ClassLoader, String, Class, ProtectionDomain, byte[])} of + * all ClassFileTransformers extracted via a {@link TransformerExtractor} and saves the transformed byte code according to the provided {@link ExportStrategy export strategy} + * on a local file. + * + * @param jarInfo information of a loaded jar file such as all the class files it contains. + */ + protected void applyInstrumentation(JarInfo jarInfo) { + log.info(PreprocessConstants.MESSAGE_PREFIX + "Applying transformation on: " + jarInfo.getFile().getAbsolutePath()); + log.info(PreprocessConstants.MESSAGE_PREFIX + "Classes found: " + jarInfo.getClassByteCodeMap().size()); + + for (Map.Entry entry : jarInfo.getClassByteCodeMap().entrySet()) { + try { + for (ClassFileTransformer transformer : TransformerExtractor.getTransformers()) { + final String internalName = entry.getKey().replace('.','/'); + final byte[] bytecodeToTransform = getInstrumentedClasses().containsKey(internalName) ? + getInstrumentedClasses().get(internalName).getClassBytes() : entry.getValue(); + + transformer.transform(ClassLoader.getSystemClassLoader(), entry.getKey(), null, null, bytecodeToTransform); + } + } catch (IllegalClassFormatException e) { + throw new InstrumentationException("Failed to instrument : " + entry.getKey(), e); + } + } + + log.debug(PreprocessConstants.MESSAGE_PREFIX + getInstrumentedClasses().size() + " classes transformed"); + jarInfo.getExportStrategy().export(jarInfo, getInstrumentedClasses(), config); + + // empty the map in preparation for transforming another package + getInstrumentedClasses().clear(); + } + + /** + * Fetches instrumented classes from the listener attached to all {@link software.amazon.disco.agent.interception.Installable installables}. + * + * @return a Map of class name as key and {@link InstrumentedClassState} as value + */ + protected Map getInstrumentedClasses() { + return TransformationListener.getInstrumentedTypes(); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java index 1f83a5a..f132fb0 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListener.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package software.amazon.disco.instrumentation.preprocess.instrumentation; import lombok.Getter; @@ -7,7 +22,7 @@ import net.bytebuddy.utility.JavaModule; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; +import software.amazon.disco.instrumentation.preprocess.exceptions.InstrumentationException; import java.util.HashMap; import java.util.Map; @@ -58,7 +73,7 @@ public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, * {@inheritDoc} */ public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) { - log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to instrument: " + typeName, throwable); + throw new InstrumentationException("Failed to instrument : " + typeName, throwable); } /** @@ -82,7 +97,7 @@ protected void collectDataFromEvent(TypeDescription typeDescription, DynamicType } if (!dynamicType.getAuxiliaryTypes().isEmpty()) { - for(Map.Entry auxiliaryEntry : dynamicType.getAuxiliaryTypes().entrySet()){ + for (Map.Entry auxiliaryEntry : dynamicType.getAuxiliaryTypes().entrySet()) { instrumentedTypes.put(auxiliaryEntry.getKey().getInternalName(), new InstrumentedClassState(null, auxiliaryEntry.getValue())); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java index fa43aa0..c2ed656 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoader.java @@ -22,6 +22,7 @@ import org.apache.logging.log4j.Logger; import software.amazon.disco.agent.DiscoAgentTemplate; import software.amazon.disco.agent.config.AgentConfig; +import software.amazon.disco.agent.config.AgentConfigParser; import software.amazon.disco.agent.inject.Injector; import software.amazon.disco.agent.interception.Installable; import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; @@ -42,7 +43,7 @@ public class DiscoAgentLoader implements AgentLoader { /** * {@inheritDoc} - * + *

* Install an agent by directly invoking the {@link Injector} api. */ @Override @@ -51,19 +52,21 @@ public void loadAgent(final PreprocessConfig config, Instrumentation instrumenta throw new NoAgentToLoadException(); } - instrumentation = instrumentation == null ? Injector.createInstrumentation() : instrumentation; - final ClassFileVersion version = parseClassFileVersionFromConfig(config); DiscoAgentTemplate.setAgentConfigFactory(() -> { - //todo if we want to pass on any args from the tool to Core, pass them here. - final AgentConfig coreConfig = new AgentConfig(null); - + final AgentConfig coreConfig = new AgentConfigParser().parseCommandLine(config.getAgentArg()); coreConfig.setAgentBuilderTransformer(getAgentBuilderTransformer(version)); + return coreConfig; }); - Injector.loadAgent(instrumentation, config.getAgentPath(), null); + // AgentConfig passed here as String will be ignored by Disco core if AgentConfigFactory is set + Injector.loadAgent( + instrumentation, + config.getAgentPath(), + null + ); } /** diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java new file mode 100644 index 0000000..8604014 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractor.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.agents; + +import lombok.Getter; +import lombok.experimental.Delegate; +import software.amazon.disco.agent.inject.Injector; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarFile; + +/** + * A No-op implementation of the {@link Instrumentation} interface that extracts all {@link ClassFileTransformer transformers} installed + * onto the instance and appends the agent jar to the bootstrap class path. + */ +public class TransformerExtractor implements Instrumentation { + @Delegate(excludes = TransformerExtractor.ExcludedListMethods.class) + private final Instrumentation delegate; + + @Getter + private static List transformers = new ArrayList<>(); + + public TransformerExtractor(Instrumentation delegate) { + this.delegate = delegate; + } + + @Override + public void addTransformer(ClassFileTransformer transformer, boolean canRetransform) { + transformers.add(transformer); + } + + @Override + public void addTransformer(ClassFileTransformer transformer) { + transformers.add(transformer); + } + + @Override + public boolean isRetransformClassesSupported() { + return true; + } + + @Override + public boolean isRedefineClassesSupported() { + return true; + } + + @Override + public void appendToBootstrapClassLoaderSearch(JarFile jarfile) { + Injector.createInstrumentation().appendToBootstrapClassLoaderSearch(jarfile); + } + + private abstract class ExcludedListMethods { + public abstract void addTransformer(ClassFileTransformer transformer, boolean canRetransform); + public abstract void appendToBootstrapClassLoaderSearch(JarFile jarfile); + public abstract boolean isRedefineClassesSupported(); + public abstract boolean isRetransformClassesSupported(); + public abstract void addTransformer(ClassFileTransformer transformer); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/ClassFileLoader.java similarity index 64% rename from disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java rename to disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/ClassFileLoader.java index 1893ef8..0b39d42 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleLoader.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/ClassFileLoader.java @@ -13,23 +13,21 @@ * permissions and limitations under the License. */ -package software.amazon.disco.instrumentation.preprocess.loaders.modules; +package software.amazon.disco.instrumentation.preprocess.loaders.classfiles; import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; import java.util.List; /** - * Interface for ModuleLoaders that load modules to be instrumented + * Interface to load byte[] of compiled classes to be instrumented */ -public interface ModuleLoader { +public interface ClassFileLoader { /** - * Loads all the modules found under the paths specified and aggregate them into a single list of {@link ModuleInfo}. - * Names of all the classes within each package are discovered and stored inside {@link ModuleInfo}. + * Loads bytecode of classes and aggregate them into a single list of {@link JarInfo}. * * @param config a PreprocessConfig containing information to perform module instrumentation - * - * @return list of {@link ModuleInfo} loaded by this package loader. Empty if no modules found. + * @return list of {@link JarInfo} loaded by this package loader. Empty if no modules found. */ - List loadPackages(PreprocessConfig config); + List load(PreprocessConfig config); } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarInfo.java similarity index 72% rename from disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java rename to disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarInfo.java index 999e9c4..2db6897 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/ModuleInfo.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarInfo.java @@ -13,25 +13,23 @@ * permissions and limitations under the License. */ -package software.amazon.disco.instrumentation.preprocess.loaders.modules; +package software.amazon.disco.instrumentation.preprocess.loaders.classfiles; import lombok.AllArgsConstructor; import lombok.Getter; -import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.export.ExportStrategy; import java.io.File; -import java.util.List; -import java.util.jar.JarFile; +import java.util.Map; /** - * Class that holds data of a Jar package that has been loaded by a {@link JarModuleLoader} including the export strategy + * Class that holds data of a Jar package that has been loaded by a {@link JarLoader} including the export strategy * that will be used to store the transformed classes. */ @AllArgsConstructor @Getter -public class ModuleInfo { +public class JarInfo { private final File file; - private final JarFile jarFile; - private final List classNames; - private final ModuleExportStrategy exportStrategy; + private final ExportStrategy exportStrategy; + private final Map classByteCodeMap; } diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarLoader.java new file mode 100644 index 0000000..ee0b449 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarLoader.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.classfiles; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.disco.agent.inject.Injector; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; +import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; +import software.amazon.disco.instrumentation.preprocess.export.ExportStrategy; +import software.amazon.disco.instrumentation.preprocess.export.JarExportStrategy; +import software.amazon.disco.instrumentation.preprocess.util.JarFileUtils; +import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * A {@link ClassFileLoader} that loads all Jar files under specified paths + */ +public class JarLoader implements ClassFileLoader { + private static final Logger log = LogManager.getLogger(JarLoader.class); + + /** + * {@inheritDoc} + */ + @Override + public List load(PreprocessConfig config) { + log.info(PreprocessConstants.MESSAGE_PREFIX + "Loading packages"); + + if (config == null || config.getJarPaths() == null) { + throw new NoModuleToInstrumentException(); + } + + final List packageEntries = new ArrayList<>(); + + for (String path : config.getJarPaths()) { + final JarInfo info = loadJar(new File(path), new JarExportStrategy()); + + if (info != null) { + packageEntries.add(info); + } + } + + if (packageEntries.isEmpty()) { + throw new NoModuleToInstrumentException(); + } + return packageEntries; + } + + /** + * Helper method to load one single Jar file + * + * @param file Jar file to be loaded + * @return {@link JarInfo object} containing package data, null if file is not a valid {@link JarFile} + */ + protected JarInfo loadJar(final File file, final ExportStrategy strategy) { + log.info(PreprocessConstants.MESSAGE_PREFIX + "Loading module: " + file.getAbsolutePath()); + + try (JarFile jarFile = new JarFile(file)) { + final Map types = new HashMap<>(); + + if (jarFile == null) { + log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to load module: "+file.getAbsolutePath()); + return null; + } + + injectFileToSystemClassPath(file); + + log.info(PreprocessConstants.MESSAGE_PREFIX + "Module loaded"); + + for (JarEntry entry : extractEntries(jarFile)) { + if (entry.getName().endsWith(".class")) { + final String nameWithoutExtension = entry.getName().substring(0, entry.getName().lastIndexOf(".class")).replace('/', '.'); + + types.put(nameWithoutExtension, JarFileUtils.readEntryFromJar(jarFile, entry)); + } + } + + return types.isEmpty() ? null : new JarInfo(file, strategy, types); + } catch (IOException e) { + log.error(PreprocessConstants.MESSAGE_PREFIX + "Invalid file, skipped", e); + return null; + } + } + + /** + * Helper method that iterates and extracts {@link JarEntry entries} that are class files + * + * @param jarFile Jar to explore + * @return a list of {@link JarEntry entries} that are class files + */ + protected List extractEntries(final JarFile jarFile) { + final List result = new ArrayList<>(); + + if (jarFile != null) { + final Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry e = entries.nextElement(); + + if (!e.isDirectory() && e.getName().endsWith(".class")) { + result.add(e); + } + } + } + return result; + } + + /** + * Add the file to the system class path using the {@link Injector injector} api. + * + * @param file Jar containing a set of classes to be added to the class path + */ + protected void injectFileToSystemClassPath(final File file) { + log.debug(PreprocessConstants.MESSAGE_PREFIX + "Injecting file to system class path: " + file.getName()); + Injector.addToSystemClasspath(Injector.createInstrumentation(), file); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java deleted file mode 100644 index 9cd3368..0000000 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoader.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.instrumentation.preprocess.loaders.modules; - -import lombok.Getter; -import software.amazon.disco.agent.inject.Injector; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; -import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; -import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; -import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; -import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.List; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -/** - * A {@link ModuleLoader} that loads all Jar files under specified paths - */ -public class JarModuleLoader implements ModuleLoader { - private static final Logger log = LogManager.getLogger(JarModuleLoader.class); - - @Getter - private final ModuleExportStrategy strategy; - - /** - * Default constructor that sets {@link #strategy} to {@link JarModuleExportStrategy} - */ - public JarModuleLoader() { - this.strategy = new JarModuleExportStrategy(); - } - - /** - * Constructor accepting a custom export strategy - * - * @param strategy {@link ModuleExportStrategy strategy} for exporting transformed classes under this path. Default strategy is {@link JarModuleExportStrategy} - */ - public JarModuleLoader(final ModuleExportStrategy strategy) { - this.strategy = strategy; - } - - /** - * {@inheritDoc} - */ - @Override - public List loadPackages(PreprocessConfig config) { - if (config == null || config.getJarPaths() == null) { - throw new NoModuleToInstrumentException(); - } - - final List packageEntries = new ArrayList<>(); - - for (String path : config.getJarPaths()) { - for (File file : discoverFilesInPath(path)) { - final ModuleInfo info = loadPackage(file); - - if (info != null) { - packageEntries.add(info); - } - } - } - if (packageEntries.isEmpty()) { - throw new NoModuleToInstrumentException(); - } - return packageEntries; - } - - /** - * Discovers all files under a path - * - * @return a List of {@link File files}, empty is no files found - */ - protected List discoverFilesInPath(final String path) { - final List files = new ArrayList<>(); - final File packageDir = new File(path); - - final File[] packageFiles = packageDir.listFiles(); - - if (packageFiles == null) { - log.debug(PreprocessConstants.MESSAGE_PREFIX + "No packages found under path: " + path); - return files; - } - - files.addAll(Arrays.asList(packageFiles)); - return files; - } - - /** - * Helper method to load one single Jar file - * - * @param file Jar file to be loaded - * @return {@link ModuleInfo object} containing package data, null if file is not a valid {@link JarFile} - */ - protected ModuleInfo loadPackage(final File file) { - final JarFile jarFile = processFile(file); - - if (jarFile == null) return null; - - final List names = new ArrayList<>(); - - for (JarEntry entry : extractEntries(jarFile)) { - names.add(entry.getName().substring(0, entry.getName().lastIndexOf(".class")).replace('/', '.')); - } - - return names.isEmpty() ? null : new ModuleInfo(file, jarFile, names, strategy); - } - - /** - * Helper method that iterates and extracts {@link JarEntry entries} that are class files - * - * @param jarFile Jar to explore - * @return a list of {@link JarEntry entries} that are class files - */ - protected List extractEntries(final JarFile jarFile) { - final List result = new ArrayList<>(); - - if (jarFile != null) { - final Enumeration entries = jarFile.entries(); - - while (entries.hasMoreElements()) { - JarEntry e = entries.nextElement(); - - if (!e.isDirectory() && e.getName().endsWith(".class")) { - result.add(e); - } - } - } - return result; - } - - /** - * Validates the file and adds it to the system class path - * - * @param file file to process - * @return a valid {@link JarFile}, null if Jar cannot be created from {@link File} passed in. - */ - protected JarFile processFile(final File file) { - if (file.isDirectory() || !file.getName().toLowerCase().contains(PreprocessConstants.JAR_EXTENSION)) return null; - - final JarFile jar = makeJarFile(file); - - if (jar != null) { - injectFileToSystemClassPath(file); - } - return jar; - } - - /** - * Creates a {@link JarFile} from {@link File}. - * - * @param file file to construct the Jar file from - * @return a valid {@link JarFile}, null if invalid - */ - protected JarFile makeJarFile(final File file) { - try { - return new JarFile(file); - } catch (IOException e) { - log.error(PreprocessConstants.MESSAGE_PREFIX + "Failed to create JarFile from file: " + file.getName(), e); - return null; - } - } - - /** - * Add the file to the system class path using the {@link Injector injector} api. - * - * @param file Jar containing a set of classes to be added to the class path - */ - protected void injectFileToSystemClassPath(final File file) { - log.debug(PreprocessConstants.MESSAGE_PREFIX + "Injecting file to system class path: " + file.getName()); - Injector.addToSystemClasspath(Injector.createInstrumentation(), file); - } -} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java new file mode 100644 index 0000000..2411149 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/JarFileUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.util; + +import software.amazon.disco.instrumentation.preprocess.exceptions.JarEntryReadException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Utility class for performing JarFile related tasks. + */ +public class JarFileUtils { + + /** + * Reads the byte[] of a JarEntry from a JarFile + * + * @param jarfile JarFile where the binary data will be read + * @param entry JarEntry to be read + * @return byte[] of the entry + * @throws JarEntryReadException + */ + public static byte[] readEntryFromJar(JarFile jarfile, JarEntry entry) { + try (final InputStream entryStream = jarfile.getInputStream(entry)) { + final byte[] buffer = new byte[2048]; + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + for (int len = entryStream.read(buffer); len != -1; len = entryStream.read(buffer)) { + os.write(buffer, 0, len); + } + return os.toByteArray(); + + } catch (IOException e) { + throw new JarEntryReadException(entry.getName(), e); + } + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/PreprocessConstants.java b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/PreprocessConstants.java index 237d70b..ab58ba6 100644 --- a/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/PreprocessConstants.java +++ b/disco-java-agent-instrumentation-preprocess/src/main/java/software/amazon/disco/instrumentation/preprocess/util/PreprocessConstants.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package software.amazon.disco.instrumentation.preprocess.util; /** diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/JarUtils.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/JarUtils.java new file mode 100644 index 0000000..04fedfc --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/JarUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess; + +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +public class JarUtils { + public static File createJar(TemporaryFolder temporaryFolder, String fileName, Map entries) throws Exception { + File file = temporaryFolder.newFile(fileName + ".jar"); + + try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + for (Map.Entry entry: entries.entrySet()) { + jarOutputStream.putNextEntry(new ZipEntry(entry.getKey())); + jarOutputStream.write(entry.getValue()); + jarOutputStream.closeEntry(); + } + } + } + return file; + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/MockEntities.java similarity index 82% rename from disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java rename to disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/MockEntities.java index 18cca97..4f48e9f 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/MockEntities.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/MockEntities.java @@ -13,14 +13,14 @@ * permissions and limitations under the License. */ -package software.amazon.disco.instrumentation.preprocess.util; +package software.amazon.disco.instrumentation.preprocess; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.DynamicType; import org.mockito.Mockito; -import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.export.ExportStrategy; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; -import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; +import software.amazon.disco.instrumentation.preprocess.loaders.classfiles.JarInfo; import java.io.File; import java.util.ArrayList; @@ -62,6 +62,15 @@ public static List makeMockJarEntries() { return list; } + public static JarFile makeMockJarFile(){ + JarFile file = Mockito.mock(JarFile.class); + + Enumeration e = Collections.enumeration(makeMockJarEntriesWithPath()); + Mockito.when(file.entries()).thenReturn(e); + + return file; + } + public static Map makeInstrumentedClassesMap() { final Map classes = new HashMap<>(); final InstrumentedClassState stateOne = new InstrumentedClassState("installable_a", new byte[]{12}); @@ -74,13 +83,6 @@ public static Map makeInstrumentedClassesMap() { return classes; } - public static List makeMockFiles() { - return Arrays.asList(new File("file_a"), - new File("file_b"), - new File("file_c")); - } - - public static List makeMockPathsWithDuplicates() { return Arrays.asList("path_a", "path_a", "path_b", "path_c"); } @@ -113,22 +115,16 @@ public static DynamicType makeMockDynamicType(){ return type; } - public static ModuleInfo makeMockPackageInfo(){ - final ModuleInfo info = Mockito.mock(ModuleInfo.class); + public static JarInfo makeMockJarInfo(){ + final JarInfo info = Mockito.mock(JarInfo.class); final File mockFile = Mockito.mock(File.class); - final JarFile mockJarFile = Mockito.mock(JarFile.class); - final ModuleExportStrategy mockStrategy = Mockito.mock(ModuleExportStrategy.class); + final ExportStrategy mockStrategy = Mockito.mock(ExportStrategy.class); Mockito.lenient().when(info.getFile()).thenReturn(mockFile); - Mockito.lenient().when(info.getJarFile()).thenReturn(mockJarFile); - Mockito.lenient().when(mockJarFile.getName()).thenReturn("mock.jar"); Mockito.lenient().when(info.getExportStrategy()).thenReturn(mockStrategy); Mockito.lenient().when(mockFile.getName()).thenReturn("mock.jar"); - final Enumeration entries = Collections.enumeration(makeMockJarEntriesWithPath()); - Mockito.lenient().when(mockJarFile.entries()).thenReturn(entries); - return info; } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java index a9fca6d..0e396f9 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/cli/PreprocessConfigParserTest.java @@ -19,7 +19,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.mockito.Mockito; +import software.amazon.disco.instrumentation.preprocess.exceptions.ArgumentParserException; import java.util.Arrays; import java.util.HashSet; @@ -37,38 +37,32 @@ public void before() { preprocessConfigParser = new PreprocessConfigParser(); } - @Test + @Test(expected = ArgumentParserException.class) public void parseCommandLineReturnsNullWithNullArgs() { - Assert.assertNull(preprocessConfigParser.parseCommandLine(null)); + preprocessConfigParser.parseCommandLine(null); } - @Test - public void parseCommandLineReturnsNullWithEmptyArgs() { - Assert.assertNull(preprocessConfigParser.parseCommandLine(new String[]{})); + @Test(expected = ArgumentParserException.class) + public void parseCommandLineFailsWithEmptyArgs() { + preprocessConfigParser.parseCommandLine(new String[]{}); } - @Test - public void parseCommandLineReturnsNullWithInvalidFlag() { - String[] args = new String[]{"--help", "--suff", suffix}; - Assert.assertNull(preprocessConfigParser.parseCommandLine(args)); + @Test(expected = ArgumentParserException.class) + public void parseCommandLineFailsWithInvalidFlag() { + String[] args = new String[]{"--suff", suffix}; + preprocessConfigParser.parseCommandLine(args); } - @Test - public void parseCommandLineReturnsNullWithUnmatchedFlagAsLastArg() { - String[] args = new String[]{"--help", "--suffix"}; - Assert.assertNull(preprocessConfigParser.parseCommandLine(args)); - } - - @Test - public void parseCommandLineReturnsNullWithHelpFlag() { - String[] args = new String[]{"--help", "--suffix", suffix}; - Assert.assertNull(preprocessConfigParser.parseCommandLine(args)); + @Test(expected = ArgumentParserException.class) + public void parseCommandLineFailsWithUnmatchedFlagAsLastArg() { + String[] args = new String[]{"--suffix"}; + preprocessConfigParser.parseCommandLine(args); } - @Test - public void parseCommandLineReturnsNullWithInvalidFormat() { + @Test(expected = ArgumentParserException.class) + public void parseCommandLineFailsWithInvalidFormat() { String[] args = new String[]{"--suffix", "--verbose"}; - Assert.assertNull(preprocessConfigParser.parseCommandLine(args)); + preprocessConfigParser.parseCommandLine(args); } @Test @@ -88,7 +82,8 @@ public void parseCommandLineWorksWithFullCommandNamesAndReturnsConfigFile() { "--serializationpath", serialization, "--agentPath", agent, "--suffix", suffix, - "--javaversion", "11" + "--javaversion", "11", + "--agentarg","arg" }; PreprocessConfig config = preprocessConfigParser.parseCommandLine(args); @@ -99,6 +94,7 @@ public void parseCommandLineWorksWithFullCommandNamesAndReturnsConfigFile() { Assert.assertEquals(agent, config.getAgentPath()); Assert.assertEquals(suffix, config.getSuffix()); Assert.assertEquals("11", config.getJavaVersion()); + Assert.assertEquals("arg", config.getAgentArg()); } @Test @@ -109,7 +105,8 @@ public void testParseCommandLineWorksWithShortHandCommandNamesAndReturnsConfigFi "-sp", serialization, "-ap", agent, "-suf", suffix, - "-jv", "11" + "-jv", "11", + "-arg","arg" }; PreprocessConfig config = preprocessConfigParser.parseCommandLine(args); @@ -120,18 +117,7 @@ public void testParseCommandLineWorksWithShortHandCommandNamesAndReturnsConfigFi Assert.assertEquals(agent, config.getAgentPath()); Assert.assertEquals(suffix, config.getSuffix()); Assert.assertEquals("11", config.getJavaVersion()); - } - - @Test - public void parseCommandLineWorkWithHelpFlag() { - PreprocessConfigParser spyParser = Mockito.spy(preprocessConfigParser); - - spyParser.parseCommandLine(new String[]{"--help"}); - Mockito.verify(spyParser).printHelpText(); - Mockito.clearInvocations(spyParser); - - spyParser.parseCommandLine(new String[]{"--verbose", "--help"}); - Mockito.verify(spyParser, Mockito.never()).printHelpText(); + Assert.assertEquals("arg", config.getAgentArg()); } @Test diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarExportStrategyTest.java similarity index 71% rename from disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java rename to disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarExportStrategyTest.java index b6c2485..5b06545 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarModuleExportStrategyTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/export/JarExportStrategyTest.java @@ -22,11 +22,11 @@ import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; -import software.amazon.disco.instrumentation.preprocess.exceptions.ModuleExportException; -import software.amazon.disco.instrumentation.preprocess.exceptions.UnableToReadJarEntryException; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; +import software.amazon.disco.instrumentation.preprocess.exceptions.JarEntryReadException; import software.amazon.disco.instrumentation.preprocess.instrumentation.InstrumentedClassState; -import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; -import software.amazon.disco.instrumentation.preprocess.util.MockEntities; +import software.amazon.disco.instrumentation.preprocess.loaders.classfiles.JarInfo; +import software.amazon.disco.instrumentation.preprocess.MockEntities; import software.amazon.disco.instrumentation.preprocess.util.PreprocessConstants; import java.io.File; @@ -39,7 +39,7 @@ import java.util.jar.JarFile; import java.util.jar.JarOutputStream; -public class JarModuleExportStrategyTest { +public class JarExportStrategyTest { static final String PACKAGE_SUFFIX = "suffix"; static final String TEMP_FILE_NAME = "temp.jar"; static final String ORIGINAL_FILE_NAME = "mock.jar"; @@ -48,42 +48,45 @@ public class JarModuleExportStrategyTest { @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); - ModuleInfo mockModuleInfo; + JarInfo mockJarInfo; Map instrumented; JarFile mockJarFile; JarOutputStream mockJarOS; - JarModuleExportStrategy mockStrategy; - JarModuleExportStrategy spyStrategy; + JarExportStrategy mockStrategy; + JarExportStrategy spyStrategy; + PreprocessConfig config; @Before public void before() throws IOException { instrumented = Mockito.mock(Map.class); mockJarFile = Mockito.mock(JarFile.class); - mockStrategy = Mockito.mock(JarModuleExportStrategy.class); + mockStrategy = Mockito.mock(JarExportStrategy.class); mockJarOS = Mockito.mock(JarOutputStream.class); - spyStrategy = Mockito.spy(new JarModuleExportStrategy()); - mockModuleInfo = MockEntities.makeMockPackageInfo(); + spyStrategy = Mockito.spy(new JarExportStrategy()); + mockJarInfo = MockEntities.makeMockJarInfo(); + config = PreprocessConfig.builder().build(); + Mockito.doCallRealMethod().when(mockStrategy).export(Mockito.any(), Mockito.any(), Mockito.any()); Mockito.when(mockStrategy.createTempFile(Mockito.any())).thenReturn(tempFolder.newFile(TEMP_FILE_NAME)); } @Test public void testExportWorksAndInvokesCreateTempFile() { - mockStrategy.export(mockModuleInfo, instrumented, null); - Mockito.verify(mockStrategy).createTempFile(mockModuleInfo); + mockStrategy.export(mockJarInfo, instrumented, null); + Mockito.verify(mockStrategy).createTempFile(mockJarInfo); } @Test public void testExportWorksAndInvokesBuildOutputJar() { - mockStrategy.export(mockModuleInfo, instrumented, null); + mockStrategy.export(mockJarInfo, instrumented, null); Mockito.verify(mockStrategy).buildOutputJar(Mockito.any(), Mockito.any(), Mockito.any()); } @Test public void testExportWorksAndInvokesMoveTempFileToDestination() { - mockStrategy.export(mockModuleInfo, instrumented, null); - Mockito.verify(mockStrategy).moveTempFileToDestination(Mockito.eq(mockModuleInfo), Mockito.any(), Mockito.any()); + mockStrategy.export(mockJarInfo, instrumented, null); + Mockito.verify(mockStrategy).moveTempFileToDestination(Mockito.eq(mockJarInfo), Mockito.any(), Mockito.any()); } @Test @@ -103,9 +106,10 @@ public void testSaveTransformedClassesWorksAndCreatesNewEntries() throws IOExcep @Test public void testCopyExistingJarEntriesWorksWithFilesAndPath() throws IOException { + JarFile jarFile = MockEntities.makeMockJarFile(); Mockito.doCallRealMethod().when(mockStrategy).copyExistingJarEntries(Mockito.eq(mockJarOS), Mockito.any(), Mockito.any()); - mockStrategy.copyExistingJarEntries(mockJarOS, mockModuleInfo, MockEntities.makeInstrumentedClassesMap()); + mockStrategy.copyExistingJarEntries(mockJarOS, jarFile, MockEntities.makeInstrumentedClassesMap()); // 3 out of 6 classes have not been instrumented Mockito.verify(mockStrategy, Mockito.times(3)).copyJarEntry(Mockito.eq(mockJarOS), Mockito.any(), Mockito.any()); @@ -116,7 +120,7 @@ public void testCopyExistingJarEntriesWorksWithFilesAndPath() throws IOException Assert.assertTrue(jarEntryArgument.getValue().getName().equals("pathA/")); } - @Test(expected = UnableToReadJarEntryException.class) + @Test(expected = JarEntryReadException.class) public void testCopyJarEntryFailsWithNullEntry() throws IOException { Mockito.doReturn(null).when(mockJarFile).getInputStream(Mockito.any()); @@ -125,14 +129,15 @@ public void testCopyJarEntryFailsWithNullEntry() throws IOException { @Test public void testCopyJarEntryWorksAndWritesToOS() throws IOException { + JarEntry entry = Mockito.mock(JarEntry.class); InputStream mockStream = Mockito.mock(InputStream.class); - Mockito.when(mockJarFile.getInputStream(null)).thenReturn(mockStream); + Mockito.when(mockJarFile.getInputStream(entry)).thenReturn(mockStream); Mockito.when(mockStream.read(Mockito.any())).thenReturn(1).thenReturn(-1); - spyStrategy.copyJarEntry(mockJarOS, mockJarFile, null); + spyStrategy.copyJarEntry(mockJarOS, mockJarFile, entry); - Mockito.verify(mockJarOS).putNextEntry(null); + Mockito.verify(mockJarOS).putNextEntry(entry); Mockito.verify(mockJarOS).write(Mockito.any(), Mockito.eq(0), Mockito.eq(1)); Mockito.verify(mockJarOS).closeEntry(); } @@ -146,11 +151,11 @@ public void testMoveTempFileToDestinationToReplaceOriginal() throws IOException long originalLength = originalFile.length(); // create temp file named temp.jar - Mockito.when(mockModuleInfo.getFile()).thenReturn(originalFile); - File file = spyStrategy.createTempFile(mockModuleInfo); + Mockito.when(mockJarInfo.getFile()).thenReturn(originalFile); + File file = spyStrategy.createTempFile(mockJarInfo); // replace original file - Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, null, file); + Path path = spyStrategy.moveTempFileToDestination(mockJarInfo, config, file); Assert.assertNotEquals(originalLength, path.toFile().length()); Assert.assertEquals(originalFile.getAbsolutePath(), path.toFile().getAbsolutePath()); @@ -161,21 +166,22 @@ public void testMoveTempFileToDestinationToReplaceOriginal() throws IOException @Test public void testMoveTempFileToDestinationWorks() throws IOException { File outDir = tempFolder.newFolder(OUT_DIR); - spyStrategy = new JarModuleExportStrategy(outDir.getAbsolutePath()); + config = PreprocessConfig.builder().outputDir(outDir.getAbsolutePath()).build(); + JarExportStrategy spyStrategy = new JarExportStrategy(); // create original file and assume temp/disco/tests is where the original package is File originalFile = createOriginalFile(); Assert.assertEquals(1, originalFile.length()); // create temp file named temp.jar - Mockito.when(mockModuleInfo.getFile()).thenReturn(originalFile); - File file = spyStrategy.createTempFile(mockModuleInfo); + Mockito.when(mockJarInfo.getFile()).thenReturn(originalFile); + File file = spyStrategy.createTempFile(mockJarInfo); // move to destination - Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, null, file); + Path path = spyStrategy.moveTempFileToDestination(mockJarInfo, config, file); Assert.assertEquals(outDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); - Assert.assertEquals(mockModuleInfo.getFile().getName(), path.toFile().getName()); + Assert.assertEquals(mockJarInfo.getFile().getName(), path.toFile().getName()); Assert.assertEquals(ORIGINAL_FILE_NAME, path.toFile().getName()); Assert.assertNotEquals(originalFile.length(), path.toFile().length()); Assert.assertTrue(originalFile.exists()); @@ -183,25 +189,26 @@ public void testMoveTempFileToDestinationWorks() throws IOException { @Test public void testMoveTempFileToDestinationWorksWithSuffix() throws IOException { - File outputDir = tempFolder.newFolder(OUT_DIR); - spyStrategy = new JarModuleExportStrategy(outputDir.getAbsolutePath()); + File outDir = tempFolder.newFolder(OUT_DIR); + config = PreprocessConfig.builder().suffix(PACKAGE_SUFFIX).outputDir(outDir.getAbsolutePath()).build(); + spyStrategy = new JarExportStrategy(); // create original file and assume temp/disco/tests is where the original package is File originalFile = createOriginalFile(); - File tempFile = spyStrategy.createTempFile(mockModuleInfo); + File tempFile = spyStrategy.createTempFile(mockJarInfo); // move to destination - Mockito.when(mockModuleInfo.getFile()).thenReturn(originalFile); - Path path = spyStrategy.moveTempFileToDestination(mockModuleInfo, PACKAGE_SUFFIX, tempFile); + Mockito.when(mockJarInfo.getFile()).thenReturn(originalFile); + Path path = spyStrategy.moveTempFileToDestination(mockJarInfo, config, tempFile); - String nameToCheck = mockModuleInfo.getFile() + String nameToCheck = mockJarInfo.getFile() .getName() - .substring(0, mockModuleInfo.getFile().getName().lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + .substring(0, mockJarInfo.getFile().getName().lastIndexOf(PreprocessConstants.JAR_EXTENSION)) + PACKAGE_SUFFIX + PreprocessConstants.JAR_EXTENSION; - Assert.assertEquals(outputDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); + Assert.assertEquals(outDir.getAbsolutePath(), path.toFile().getParentFile().getAbsolutePath()); Assert.assertEquals(nameToCheck, path.toFile().getName()); Assert.assertTrue(originalFile.exists()); } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/StaticInstrumentationTransformerTest.java similarity index 53% rename from disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java rename to disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/StaticInstrumentationTransformerTest.java index a54ac0f..7e8e56d 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/ModuleTransformerTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/StaticInstrumentationTransformerTest.java @@ -17,6 +17,7 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -25,32 +26,37 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; -import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; +import software.amazon.disco.instrumentation.preprocess.exceptions.InvalidConfigEntryException; +import software.amazon.disco.instrumentation.preprocess.export.JarExportStrategy; import software.amazon.disco.instrumentation.preprocess.loaders.agents.DiscoAgentLoader; -import software.amazon.disco.instrumentation.preprocess.loaders.modules.JarModuleLoader; -import software.amazon.disco.instrumentation.preprocess.loaders.modules.ModuleInfo; -import software.amazon.disco.instrumentation.preprocess.util.MockEntities; - +import software.amazon.disco.instrumentation.preprocess.loaders.agents.TransformerExtractor; +import software.amazon.disco.instrumentation.preprocess.loaders.classfiles.JarInfo; +import software.amazon.disco.instrumentation.preprocess.loaders.classfiles.JarLoader; +import software.amazon.disco.instrumentation.preprocess.MockEntities; + +import java.io.File; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; +import java.util.HashMap; import java.util.Map; @RunWith(MockitoJUnitRunner.class) -public class ModuleTransformerTest { +public class StaticInstrumentationTransformerTest { private static final String PACKAGE_SUFFIX = "suffix"; - ModuleTransformer spyTransformer; + StaticInstrumentationTransformer spyTransformer; + PreprocessConfig config; @Mock DiscoAgentLoader mockAgentLoader; @Mock - JarModuleLoader mockJarPackageLoader; + JarLoader mockJarPackageLoader; - PreprocessConfig config; - List moduleInfos; + @Mock + JarInfo jarInfo; @Before public void before() { @@ -61,26 +67,28 @@ public void before() { .build(); spyTransformer = Mockito.spy( - ModuleTransformer.builder() + StaticInstrumentationTransformer.builder() .jarLoader(mockJarPackageLoader) .agentLoader(mockAgentLoader) .config(config) .build() ); - Mockito.doReturn(Arrays.asList(MockEntities.makeMockPackageInfo())) - .when(mockJarPackageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); - - moduleInfos = new ArrayList<>(); - moduleInfos.add(Mockito.mock(ModuleInfo.class)); - moduleInfos.add(Mockito.mock(ModuleInfo.class)); + Mockito.doReturn(Arrays.asList(MockEntities.makeMockJarInfo())) + .when(mockJarPackageLoader).load(Mockito.any(PreprocessConfig.class)); } - @Test - public void testTransformWorksWithDefaultLogLevel() { - spyTransformer.transform(); + @After + public void after(){ + TransformerExtractor.getTransformers().clear(); + } - Assert.assertEquals(LogManager.getLogger().getLevel(), Level.INFO); + @Test(expected = InvalidConfigEntryException.class) + public void testTransformFailsWithNullConfig(){ + StaticInstrumentationTransformer.builder() + .config(null) + .build() + .transform(); } @Test @@ -91,7 +99,7 @@ public void testTransformWorksWithVerboseLogLevel() { .logLevel(Level.TRACE) .build(); - ModuleTransformer.builder() + StaticInstrumentationTransformer.builder() .jarLoader(mockJarPackageLoader) .agentLoader(mockAgentLoader) .config(config) @@ -100,10 +108,16 @@ public void testTransformWorksWithVerboseLogLevel() { Assert.assertEquals(Level.TRACE, LogManager.getLogger().getLevel()); } + @Test + public void testTransformWorksWithDefaultLogLevel() { + spyTransformer.transform(); + Assert.assertEquals(LogManager.getLogger().getLevel(), Level.INFO); + } + @Test public void testTransformWorksAndInvokesLoadAgentAndPackages() { spyTransformer = Mockito.spy( - ModuleTransformer.builder() + StaticInstrumentationTransformer.builder() .jarLoader(mockJarPackageLoader) .agentLoader(mockAgentLoader) .config(config) @@ -111,33 +125,46 @@ public void testTransformWorksAndInvokesLoadAgentAndPackages() { ); spyTransformer.transform(); - Mockito.verify(mockAgentLoader).loadAgent(Mockito.any(PreprocessConfig.class), Mockito.any(Instrumentation.class)); - Mockito.verify(mockJarPackageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); + Mockito.verify(mockAgentLoader).loadAgent(Mockito.any(PreprocessConfig.class), Mockito.any(TransformerExtractor.class)); + Mockito.verify(mockJarPackageLoader).load(Mockito.any(PreprocessConfig.class)); } @Test public void testTransformWorksAndInvokesPackageLoader() { spyTransformer.transform(); - Mockito.verify(mockJarPackageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); + Mockito.verify(mockJarPackageLoader).load(Mockito.any(PreprocessConfig.class)); Mockito.verify(spyTransformer).applyInstrumentation(Mockito.any()); } - @Test - public void testApplyInstrumentationWorksAndInvokesExport() { - Mockito.doCallRealMethod().when(spyTransformer).applyInstrumentation(Mockito.any()); - - JarModuleExportStrategy s1 = Mockito.mock(JarModuleExportStrategy.class); - Mockito.when(moduleInfos.get(0).getExportStrategy()).thenReturn(s1); - + public void testApplyInstrumentationWorks() throws IllegalClassFormatException { + Instrumentation delegate = Mockito.mock(Instrumentation.class); + JarExportStrategy strategy = Mockito.mock(JarExportStrategy.class); Map instrumentedClasses = MockEntities.makeInstrumentedClassesMap(); + File file = Mockito.mock(File.class); + + Map byteArrayMap = new HashMap<>(); + byteArrayMap.put("ClassA", new byte[]{1}); + byteArrayMap.put("ClassB", new byte[]{2}); + + TransformerExtractor transformerExtractor = new TransformerExtractor(delegate); + ClassFileTransformer transformer_1 = Mockito.mock(ClassFileTransformer.class); + ClassFileTransformer transformer_2 = Mockito.mock(ClassFileTransformer.class); + transformerExtractor.addTransformer(transformer_1); + transformerExtractor.addTransformer(transformer_2); + + Mockito.when(jarInfo.getExportStrategy()).thenReturn(strategy); + Mockito.when(jarInfo.getClassByteCodeMap()).thenReturn(byteArrayMap); + Mockito.when(jarInfo.getFile()).thenReturn(file); + Mockito.when(file.getAbsolutePath()).thenReturn("mock/path"); Mockito.doReturn(instrumentedClasses).when(spyTransformer).getInstrumentedClasses(); - spyTransformer.applyInstrumentation(moduleInfos.get(0)); + spyTransformer.applyInstrumentation(jarInfo); - Mockito.verify(moduleInfos.get(0)).getClassNames(); - Mockito.verify(s1).export(moduleInfos.get(0), instrumentedClasses, PACKAGE_SUFFIX); + Mockito.verify(strategy).export(jarInfo, instrumentedClasses, config); + Mockito.verify(transformer_1).transform(Mockito.any(ClassLoader.class), Mockito.eq("ClassA"), Mockito.eq(null), Mockito.eq(null), Mockito.eq(new byte[]{1})); + Mockito.verify(transformer_1).transform(Mockito.any(ClassLoader.class), Mockito.eq("ClassB"), Mockito.eq(null), Mockito.eq(null), Mockito.eq(new byte[]{2})); Assert.assertTrue(instrumentedClasses.isEmpty()); } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java index b05462e..2c42fcf 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/instrumentation/TransformationListenerTest.java @@ -2,6 +2,7 @@ import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.DynamicType; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -9,7 +10,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -import software.amazon.disco.instrumentation.preprocess.util.MockEntities; +import software.amazon.disco.instrumentation.preprocess.MockEntities; @RunWith(MockitoJUnitRunner.class) public class TransformationListenerTest { @@ -28,6 +29,11 @@ public void before() { Mockito.when(mockTypeDescription.getInternalName()).thenReturn(MockEntities.makeClassPaths().get(0)); } + @After + public void after() { + TransformationListener.getInstrumentedTypes().clear(); + } + @Test public void testOnTransformationWorksAndInvokesCollectDataFromEvent() { Mockito.doCallRealMethod().when(mockListener).onTransformation(mockTypeDescription, null, null, false, mockDynamicTypeWithAuxiliary); diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java index 4aa5cb1..1bc72fd 100644 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/DiscoAgentLoaderTest.java @@ -30,14 +30,14 @@ import software.amazon.disco.instrumentation.preprocess.exceptions.InvalidConfigEntryException; import software.amazon.disco.instrumentation.preprocess.exceptions.NoAgentToLoadException; import software.amazon.disco.instrumentation.preprocess.instrumentation.TransformationListener; +import software.amazon.disco.instrumentation.preprocess.JarUtils; import java.io.File; -import java.io.FileOutputStream; import java.lang.instrument.Instrumentation; +import java.util.HashMap; +import java.util.Map; import java.util.function.Supplier; import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; -import java.util.zip.ZipEntry; public class DiscoAgentLoaderTest { @Rule @@ -49,7 +49,7 @@ public void testLoadAgentFailOnNullPaths() throws NoAgentToLoadException { } @Test - public void testParsingJavaVersionWorks(){ + public void testParsingJavaVersionWorks() { PreprocessConfig config = PreprocessConfig.builder() .agentPath("path") .javaVersion("11") @@ -66,7 +66,7 @@ public void testParsingJavaVersionWorks(){ } @Test(expected = InvalidConfigEntryException.class) - public void testParsingJavaVersionFailsWithInvalidJavaVersion(){ + public void testParsingJavaVersionFailsWithInvalidJavaVersion() { PreprocessConfig config = PreprocessConfig.builder() .agentPath("path") .javaVersion("a version") @@ -76,11 +76,15 @@ public void testParsingJavaVersionFailsWithInvalidJavaVersion(){ @Test public void testLoadAgentRegistersAgentBuilderTransformerAndInstallsAgent() throws Exception { - Instrumentation instrumentation = Mockito.mock(Instrumentation.class); + Instrumentation delegate = Mockito.mock(Instrumentation.class); + Instrumentation instrumentation = Mockito.spy(new TransformerExtractor(delegate)); AgentBuilder agentBuilder = Mockito.mock(AgentBuilder.class); Mockito.when(agentBuilder.with(Mockito.any(ByteBuddy.class))).thenReturn(agentBuilder); - File file = createJar("TestJarFile"); + Map entries = new HashMap<>(); + entries.put("Foo.class", "Foo.class".getBytes()); + + File file = JarUtils.createJar(temporaryFolder, "TestJarFile", entries); PreprocessConfig config = PreprocessConfig.builder().agentPath(file.getAbsolutePath()).build(); Assert.assertNull(DiscoAgentTemplate.getAgentConfigFactory()); @@ -95,8 +99,7 @@ public void testLoadAgentRegistersAgentBuilderTransformerAndInstallsAgent() thro agentConfigSupplier.get().getAgentBuilderTransformer().apply(agentBuilder, null); Mockito.verify(agentBuilder).with(Mockito.any(TransformationListener.class)); - // check if a ByteBuddy instance with the correct java version is being installed using its own - // equals method + // check if a ByteBuddy instance with the correct java version is being installed using its own equals method Assert.assertEquals(ClassFileVersion.JAVA_V8, DiscoAgentLoader.parseClassFileVersionFromConfig(config)); ArgumentCaptor byteBuddyArgumentCaptor = ArgumentCaptor.forClass(ByteBuddy.class); Mockito.verify(agentBuilder).with(byteBuddyArgumentCaptor.capture()); @@ -107,32 +110,4 @@ public void testLoadAgentRegistersAgentBuilderTransformerAndInstallsAgent() thro Mockito.verify(instrumentation).appendToBootstrapClassLoaderSearch(jarFileArgumentCaptor.capture()); Assert.assertEquals(file.getAbsolutePath(), jarFileArgumentCaptor.getValue().getName()); } - - private File createJar(String name) throws Exception { - File file = temporaryFolder.newFile(name+".jar"); - try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { - try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { - //write a sentinal file with the same name as the jar, to test if it becomes readable by getResource. - jarOutputStream.putNextEntry(new ZipEntry(name)); - jarOutputStream.write("foobar".getBytes()); - jarOutputStream.closeEntry(); - } - } - return file; - } - -// class MockAgentBuilderTransformer implements BiFunction { -// @Override -// public AgentBuilder apply(AgentBuilder agentBuilder, Installable installable) { -// return agentBuilder -// .with(new ByteBuddy(version)) -// .with(new TransformationListener(uuidGenerate(installable))); -// } -// -// class ByteBuddyTest extends ByteBuddy{ -// public ClassFileVersion getClassFileVersion(){ -// return classFileVersion; -// } -// } -// } } diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java new file mode 100644 index 0000000..64d5449 --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/agents/TransformerExtractorTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.agents; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; + +public class TransformerExtractorTest { + @After + public void after(){ + TransformerExtractor.getTransformers().clear(); + } + + @Test + public void testAddTransformerWorks(){ + ClassFileTransformer classFileTransformer = Mockito.mock(ClassFileTransformer.class); + Instrumentation delegate = Mockito.mock(Instrumentation.class); + + TransformerExtractor extractor = new TransformerExtractor(delegate); + extractor.addTransformer(classFileTransformer); + extractor.addTransformer(classFileTransformer); + extractor.addTransformer(classFileTransformer); + + Assert.assertEquals(3, TransformerExtractor.getTransformers().size()); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarLoaderTest.java new file mode 100644 index 0000000..f5b1fbb --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/classfiles/JarLoaderTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.loaders.classfiles; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; +import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; +import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; +import software.amazon.disco.instrumentation.preprocess.export.ExportStrategy; +import software.amazon.disco.instrumentation.preprocess.JarUtils; +import software.amazon.disco.instrumentation.preprocess.MockEntities; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class JarLoaderTest { + JarLoader loader; + PreprocessConfig config; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void before() { + config = PreprocessConfig.builder().jarPaths(MockEntities.makeMockPathsWithDuplicates()).build(); + loader = new JarLoader(); + } + + @Test(expected = NoModuleToInstrumentException.class) + public void testLoadFailWithEmptyPathList() { + loader.load(config); + } + + @Test(expected = NoModuleToInstrumentException.class) + public void testLoadFailWithNullConfig() { + loader.load(null); + } + + @Test(expected = NoModuleToInstrumentException.class) + public void testLoadFailWithNullPathList() { + loader.load(PreprocessConfig.builder().build()); + } + + @Test + public void testLoadWorksWithMultiplePaths() { + JarLoader packageLoader = Mockito.mock(JarLoader.class); + JarInfo info = Mockito.mock(JarInfo.class); + + Mockito.doCallRealMethod().when(packageLoader).load(config); + Mockito.doReturn(info).when(packageLoader).loadJar(Mockito.any(File.class), Mockito.any(ExportStrategy.class)); + + List infos = packageLoader.load(config); + + Mockito.verify(packageLoader, Mockito.times(3)).loadJar(Mockito.any(File.class), Mockito.any(ExportStrategy.class)); + Assert.assertEquals(3, infos.size()); + } + + @Test + public void testLoadJarWorks() throws Exception { + JarLoader packageLoader = Mockito.spy(new JarLoader()); + + Map srcEntries = new HashMap<>(); + srcEntries.put("A.class", "A.class".getBytes()); + srcEntries.put("B.class", "B.class".getBytes()); + + File file = JarUtils.createJar(temporaryFolder, "jarFile", srcEntries); + + JarInfo info = packageLoader.loadJar(file, null); + + Mockito.verify(packageLoader).injectFileToSystemClassPath(file); + Assert.assertEquals(2, info.getClassByteCodeMap().size()); + Assert.assertEquals(file, info.getFile()); + Assert.assertTrue(info.getClassByteCodeMap().containsKey("A")); + Assert.assertTrue(info.getClassByteCodeMap().containsKey("B")); + Assert.assertArrayEquals("A.class".getBytes(), info.getClassByteCodeMap().get("A")); + Assert.assertArrayEquals("B.class".getBytes(), info.getClassByteCodeMap().get("B")); + } + + @Test + public void testExtractEntriesWorks() { + JarFile jarFile = MockEntities.makeMockJarFile(); + + List entries = loader.extractEntries(jarFile); + + Assert.assertEquals(6, entries.size()); + } +} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java deleted file mode 100644 index 2d660d0..0000000 --- a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/loaders/modules/JarModuleLoaderTest.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.disco.instrumentation.preprocess.loaders.modules; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; -import software.amazon.disco.instrumentation.preprocess.cli.PreprocessConfig; -import software.amazon.disco.instrumentation.preprocess.exceptions.NoModuleToInstrumentException; -import software.amazon.disco.instrumentation.preprocess.export.JarModuleExportStrategy; -import software.amazon.disco.instrumentation.preprocess.export.ModuleExportStrategy; -import software.amazon.disco.instrumentation.preprocess.util.MockEntities; - -import java.io.File; -import java.util.Arrays; -import java.util.List; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.stream.Collectors; - -@RunWith(MockitoJUnitRunner.class) -public class JarModuleLoaderTest { - static final List PATHS = MockEntities.makeMockPathsWithDuplicates(); - static final List MOCK_FILES = MockEntities.makeMockFiles(); - static final List MOCK_JAR_ENTRIES = MockEntities.makeMockJarEntries(); - - JarModuleLoader loader; - PreprocessConfig config; - - @Mock - JarFile jarFile; - - @Mock - File mockFile; - - @Before - public void before(){ - config = PreprocessConfig.builder().jarPaths(PATHS).build(); - loader = new JarModuleLoader(); - - Mockito.when(mockFile.isDirectory()).thenReturn(false); - Mockito.when(mockFile.getName()).thenReturn("ATestJar.jar"); - } - - @Test(expected = NoModuleToInstrumentException.class) - public void testConstructorFailWithEmptyPathList() { - new JarModuleLoader().loadPackages(config); - } - - @Test(expected = NoModuleToInstrumentException.class) - public void testConstructorFailWithNullConfig() { - new JarModuleLoader().loadPackages(null); - } - - @Test(expected = NoModuleToInstrumentException.class) - public void testConstructorFailWithNullPathList() { - new JarModuleLoader().loadPackages(PreprocessConfig.builder().build()); - } - - @Test - public void testConstructorWorksAndHasDefaultStrategy() { - Assert.assertTrue(loader.getStrategy().getClass().equals(JarModuleExportStrategy.class)); - } - - @Test - public void testConstructorWorksWithNonDefaultStrategy() { - ModuleExportStrategy mockStrategy = Mockito.mock(ModuleExportStrategy.class); - - loader = new JarModuleLoader(mockStrategy); - Assert.assertNotEquals(JarModuleExportStrategy.class, loader.getStrategy().getClass()); - } - - @Test - public void testProcessFileWorksWithValidFileExtension(){ - JarModuleLoader loader = Mockito.mock(JarModuleLoader.class); - JarFile jar = Mockito.mock(JarFile.class); - - Mockito.doCallRealMethod().when(loader).processFile(mockFile); - Mockito.doReturn(jar).when(loader).makeJarFile(mockFile); - - Assert.assertNotNull(loader.processFile(mockFile)); - - Mockito.when(mockFile.getName()).thenReturn("ATestJar.JAR"); - Assert.assertNotNull(loader.processFile(mockFile)); - } - - @Test - public void testProcessFileWorksWithInvalidFileExtensionAndReturnNull(){ - JarModuleLoader loader = Mockito.mock(JarModuleLoader.class); - - Mockito.when(mockFile.getName()).thenReturn("ATestJar.txt"); - Mockito.doCallRealMethod().when(loader).processFile(mockFile); - - Assert.assertNull(loader.processFile(mockFile)); - } - - @Test - public void testProcessFileWorksAndInvokesInjectFileToSystemClassPath() { - JarModuleLoader packageLoader = Mockito.mock(JarModuleLoader.class); - Mockito.when(packageLoader.processFile(Mockito.any())).thenCallRealMethod(); - - JarFile mockJarfile = Mockito.mock(JarFile.class); - Mockito.when(packageLoader.makeJarFile(mockFile)).thenReturn(mockJarfile); - - packageLoader.processFile(mockFile); - - Mockito.verify(packageLoader).injectFileToSystemClassPath(mockFile); - } - - @Test - public void testLoadPackagesWorksWithOnePackageInfo() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); - - Mockito.doCallRealMethod().when(packageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); - Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(0))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); - Mockito.doReturn(MockEntities.makeMockPackageInfo()).when(packageLoader).loadPackage(MOCK_FILES.get(0)); - - packageLoader.loadPackages(config); - - Mockito.verify(packageLoader).loadPackage(Mockito.any()); - } - - @Test(expected = NoModuleToInstrumentException.class) - public void testLoadPackagesFailsWithNoPackageInfoCreated() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); - - Mockito.doCallRealMethod().when(packageLoader).loadPackages(Mockito.any(PreprocessConfig.class)); - Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(0))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); - - packageLoader.loadPackages(config); - - Mockito.verify(packageLoader).loadPackage(Mockito.any()); - } - - @Test - public void testLoadPackagesWorksAndCalledThreeTimesWithThreePaths() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader()); - - Mockito.doCallRealMethod().when(packageLoader).loadPackages(config); - - Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(1))).thenReturn(Arrays.asList(MOCK_FILES.get(0))); - Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(2))).thenReturn(Arrays.asList(MOCK_FILES.get(1))); - Mockito.when(packageLoader.discoverFilesInPath(PATHS.get(3))).thenReturn(Arrays.asList(MOCK_FILES.get(2))); - - try { - packageLoader.loadPackages(config); - } catch (NoModuleToInstrumentException e) { - // swallow - } - - Mockito.verify(packageLoader, Mockito.times(3)).loadPackage(Mockito.any()); - } - - @Test - public void testLoadPackagesWorksAndReturnsValidPackageInfoObjectAndInvokesProcessFile() { - JarModuleLoader packageLoader = Mockito.spy(new JarModuleLoader(new JarModuleExportStrategy())); - - List classes = MOCK_JAR_ENTRIES - .stream() - .map(jarEntry -> jarEntry.getName().substring(0, jarEntry.getName().lastIndexOf(".class"))) - .collect(Collectors.toList()); - - Mockito.doCallRealMethod().when(packageLoader).loadPackage(MOCK_FILES.get(0)); - Mockito.doReturn(jarFile).when(packageLoader).processFile(MOCK_FILES.get(0)); - Mockito.doReturn(MOCK_JAR_ENTRIES).when(packageLoader).extractEntries(Mockito.any()); - - final ModuleInfo info = packageLoader.loadPackage(MOCK_FILES.get(0)); - - Mockito.verify(packageLoader, Mockito.times(1)).processFile(Mockito.any()); - Assert.assertTrue(info.getClassNames().size() == MOCK_JAR_ENTRIES.size()); - Assert.assertArrayEquals(classes.toArray(), info.getClassNames().toArray()); - Assert.assertSame(MOCK_FILES.get(0), info.getFile()); - Assert.assertSame(jarFile, info.getJarFile()); - Assert.assertSame(JarModuleExportStrategy.class, info.getExportStrategy().getClass()); - } -} diff --git a/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarfileUtilsTest.java b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarfileUtilsTest.java new file mode 100644 index 0000000..8b260ba --- /dev/null +++ b/disco-java-agent-instrumentation-preprocess/src/test/java/software/amazon/disco/instrumentation/preprocess/util/JarfileUtilsTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.instrumentation.preprocess.util; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import software.amazon.disco.instrumentation.preprocess.JarUtils; + +import java.io.File; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class JarfileUtilsTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testReadEntryFromJarWorks() throws Exception { + Map srcEntries = new HashMap<>(); + srcEntries.put("ClassAAAAAAAAAAAAA.class","ClassAAAAAAAAAAAAA.class".getBytes()); + srcEntries.put("ClassBBBBBBBBBBBB.class","ClassBBBBBBBBBBBB.class".getBytes()); + srcEntries.put("/CCCC","/CCCC".getBytes()); + + File testFile = JarUtils.createJar(temporaryFolder, "Test.jar", srcEntries); + + Map entriesRead = new HashMap<>(); + try (JarFile jarFile = new JarFile(testFile)) { + Enumeration jarEntries = jarFile.entries(); + + while (jarEntries.hasMoreElements()) { + JarEntry entry = jarEntries.nextElement(); + entriesRead.put(entry.getName(), JarFileUtils.readEntryFromJar(jarFile, entry)); + } + } + + Assert.assertEquals(3, entriesRead.size()); + for(Map.Entry entry: srcEntries.entrySet()){ + Assert.assertTrue(entriesRead.containsKey(entry.getKey())); + Assert.assertArrayEquals(entry.getValue(), entriesRead.get(entry.getKey())); + } + } +} From 8142daec8adc2c72fc6361c4ee796b6ec1ea3ae7 Mon Sep 17 00:00:00 2001 From: William Armiros Date: Sun, 16 Aug 2020 14:53:16 -0700 Subject: [PATCH 43/45] added aws interception library --- disco-java-agent-aws/README.md | 21 +- disco-java-agent-aws/build.gradle.kts | 7 + .../amazon/disco/agent/event/.gitkeep | 1 - .../event/AwsServiceDownstreamEvent.java | 49 ++++ .../AwsServiceDownstreamRequestEvent.java | 79 +++++++ .../AwsServiceDownstreamResponseEvent.java | 89 ++++++++ .../build.gradle.kts | 41 +++- .../agent/safety/AWSLibrariesAbsentTests.java | 11 + .../java/software/amazon/disco/agent/.gitkeep | 1 - .../amazon/disco/agent/AWSSupport.java | 25 +++ .../awsv1/AWSClientInvokeInterceptor.java | 138 ++++++++++++ .../awsv2/AWSClientBuilderInterceptor.java | 100 +++++++++ .../awsv2/DiscoExecutionInterceptor.java | 210 ++++++++++++++++++ .../AwsServiceDownstreamRequestEventImpl.java | 99 +++++++++ ...AwsServiceDownstreamResponseEventImpl.java | 103 +++++++++ ...wsV1ServiceDownstreamRequestEventImpl.java | 37 +++ .../java/software/amazon/disco/agent/.gitkeep | 1 - .../amazon/disco/agent/AWSSupportTests.java | 19 ++ .../AWSClientInvokeInterceptorTests.java | 202 +++++++++++++++++ .../AWSClientBuilderInterceptorTests.java | 126 +++++++++++ .../awsv2/DiscoExecutionInterceptorTests.java | 206 +++++++++++++++++ .../event/AwsServiceDownstreamEventTests.java | 124 +++++++++++ 22 files changed, 1682 insertions(+), 7 deletions(-) delete mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/.gitkeep create mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamEvent.java create mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEvent.java create mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamResponseEvent.java create mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/src/safetyTest/java/software/amazon/disco/agent/safety/AWSLibrariesAbsentTests.java delete mode 100644 disco-java-agent-aws/src/main/java/software/amazon/disco/agent/.gitkeep create mode 100644 disco-java-agent-aws/src/main/java/software/amazon/disco/agent/AWSSupport.java create mode 100644 disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv1/AWSClientInvokeInterceptor.java create mode 100644 disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/AWSClientBuilderInterceptor.java create mode 100644 disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptor.java create mode 100644 disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEventImpl.java create mode 100644 disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamResponseEventImpl.java create mode 100644 disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsV1ServiceDownstreamRequestEventImpl.java delete mode 100644 disco-java-agent-aws/src/test/java/software/amazon/disco/agent/.gitkeep create mode 100644 disco-java-agent-aws/src/test/java/software/amazon/disco/agent/AWSSupportTests.java create mode 100644 disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv1/AWSClientInvokeInterceptorTests.java create mode 100644 disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv2/AWSClientBuilderInterceptorTests.java create mode 100644 disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptorTests.java create mode 100644 disco-java-agent-aws/src/test/java/software/amazon/disco/agent/event/AwsServiceDownstreamEventTests.java diff --git a/disco-java-agent-aws/README.md b/disco-java-agent-aws/README.md index 22ee307..ceb779b 100644 --- a/disco-java-agent-aws/README.md +++ b/disco-java-agent-aws/README.md @@ -4,15 +4,30 @@ Serving as both an example of how to author a Disco library/plugin/api, and also Event producer for requests made using versions 1 and 2 of the AWS SDK for Java, this subproject is laid out as follows: 1. In this folder, the Installables to intercept AWS SDK requests & responses, and issue appropriate Event Bus Events. -1. In the disco-java-agent-aws-plugin subfolder, a proper Disco plugin, bundled as a plugin JAR file with Manifest. -1. In the disco-java-agent-aws-api subfolder, a collection of Event classes which are implemented & published by +1. In the `disco-java-agent-aws-plugin` subfolder, a proper Disco plugin, bundled as a plugin JAR file with Manifest. +1. In the `disco-java-agent-aws-api` subfolder, a collection of Event classes which are implemented & published by the intstallables in this package. See the READMEs of the submodules for more information on each. ## Feature Status -TODO +This installable will publish `ServiceDownstreamEvent`s for requests and responses from V1 of the AWS SDK. These +events are populated with the standard Disco metadata of origin, service name, and operation name, in addition to +supplying the Request and Response objects created by the AWS SDK. For V2 of the AWS SDK, the installable publishes +`AwsServiceDownstreamEvent`s. These events are an extension `ServiceDownstreamEvent`s, meaning they provide all the +same metadata in addition to the AWS region, number of retries, and request ID as indicated by the "additional +metadata" column in the table below. + +For both versions of the AWS SDK, the published events expose a `replaceHeader(String, String)` method to add +arbitrary headers to requests. + +| | Intercept client requests | Replace headers | Standard metadata | Additional metadata | +|----------------------------------------------------------------------------------------|---------------------------|--------------------|--------------------|---------------------------| +| [AWS SDK V1](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/welcome.html) | :heavy_check_mark: * | :heavy_check_mark: | :heavy_check_mark: | :heavy_multiplication_x: | +| [AWS SDK V2](https://docs.aws.amazon.com/sdk-for-java/v2/developer-guide/welcome.html) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | + +\* Interception for S3 clients made with V1 of the AWS SDK is not yet available ## Package description diff --git a/disco-java-agent-aws/build.gradle.kts b/disco-java-agent-aws/build.gradle.kts index d7e079c..d0a6771 100644 --- a/disco-java-agent-aws/build.gradle.kts +++ b/disco-java-agent-aws/build.gradle.kts @@ -14,9 +14,16 @@ */ dependencies { + // Compile against AWS SDK V2, but we do not take a runtime dependency on it + compileOnly("software.amazon.awssdk", "sdk-core", "2.13.76") + implementation(project(":disco-java-agent-aws:disco-java-agent-aws-api")) implementation(project(":disco-java-agent:disco-java-agent-core")) testImplementation("org.mockito", "mockito-core", "1.+") + testImplementation("com.amazonaws", "aws-java-sdk-dynamodb", "1.11.840") + testImplementation("com.amazonaws", "aws-java-sdk-sns", "1.11.840") + testImplementation("com.amazonaws", "aws-java-sdk-sqs", "1.11.840") + testImplementation("software.amazon.awssdk", "dynamodb", "2.13.76") } configure { diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/.gitkeep b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/.gitkeep deleted file mode 100644 index 1a48e48..0000000 --- a/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -TODO: Add AWS Events \ No newline at end of file diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamEvent.java b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamEvent.java new file mode 100644 index 0000000..8893b90 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamEvent.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.event; + +import java.util.List; +import java.util.Map; + +/** + * Generic interface of the Aws Service Downstream Events. Unifies common APIs that exist in both the request + * and response events. + */ +public interface AwsServiceDownstreamEvent { + + /** + * Keys to use in the data map + */ + enum DataKey { + HEADER_MAP, + } + + /** + * Obtain the Field value for the corresponding event. This is broken down into either its request or response + * stream, where the request event obtains field values from the SdkRequest object and the response event obtains + * field values from the SdkResponse object. + * @param fieldName - The field name for the Sdk Request/Response + * @param clazz - The class the return object should be casted into upon returning + * @return The object in the Sdk request/response field value. + */ + Object getValueForField(String fieldName, Class clazz); + + /** + * Obtain the header map that the particular event holds. + * @return A key-value pairing map of the headers. + */ + Map> getHeaderMap(); +} diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEvent.java b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEvent.java new file mode 100644 index 0000000..e34b60d --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEvent.java @@ -0,0 +1,79 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.event; + +import java.util.List; +import java.util.Map; + +import static software.amazon.disco.agent.event.AwsServiceDownstreamEvent.DataKey.HEADER_MAP; + +/** + * Specialization of a ServiceDownstreamRequestEvent, to encapsulate data specific to Aws downstream call requests. + * Implementation of inherited methods are in the disco-java-agent-aws package. + */ +public abstract class AwsServiceDownstreamRequestEvent extends ServiceDownstreamRequestEvent implements AwsServiceDownstreamEvent, HeaderReplaceable { + /** + * Keys to use in the data map + */ + enum DataKey { + /** + * The Request Id of the current sdk request. + */ + REQUEST_ID, + + /** + * The region of the request + */ + REGION, + + /** + * The operation name of the current request + */ + OPERATION_NAME, + + /** + * The service that the request is going to + */ + SERVICE_NAME, + } + + /** + * Construct a new AwsServiceDownstreamRequestEvent + * @param origin the origin of the downstream call e.g. 'AWSv1' or 'AWSv2' + * @param service the service name e.g. 'DynamoDb' + * @param operation the operation name e.g. 'ListTables' + */ + public AwsServiceDownstreamRequestEvent(String origin, String service, String operation) { + super(origin, service, operation); + } + + /** + * Retrieve the region for this event + * @return the region the downstream call is making to. + */ + public String getRegion() { + return (String)getData(DataKey.REGION.name()); + } + + /** + * Retrieve the underlying Http header map for the outgoing request. + * @return an immutable header map + */ + public Map> getHeaderMap() { + return (Map>) getData(HEADER_MAP.name()); + } + +} diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamResponseEvent.java b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamResponseEvent.java new file mode 100644 index 0000000..d2c5580 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamResponseEvent.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.event; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static software.amazon.disco.agent.event.AwsServiceDownstreamEvent.DataKey.HEADER_MAP; + +/** + * Specialization of a ServiceDownstreamResponseEvent, to encapsulate data specific to Aws downstream call responses. + * Implementation of inherited methods are in the disco-java-agent-aws package. + */ +public abstract class AwsServiceDownstreamResponseEvent extends ServiceDownstreamResponseEvent implements AwsServiceDownstreamEvent { + /** + * Keys to use in the data map + */ + enum DataKey { + /** + * The Request Id of the current sdk request + */ + REQUEST_ID, + + /** + * The number of retries the AWS SDK used to perform this request + */ + RETRIES + } + + /** + * Construct a new AwsServiceDownstreamResponseEvent + * @param origin the origin of the downstream call e.g. 'AWSv1' or 'AWSv2' + * @param service the service name e.g. 'DynamoDb' + * @param operation the operation name e.g. 'ListTables' + * @param requestEvent the associated request event + */ + public AwsServiceDownstreamResponseEvent(String origin, String service, String operation, ServiceDownstreamRequestEvent requestEvent) { + super(origin, service, operation, requestEvent); + } + + /** + * Obtain the overall status code of the downstream call. If we can't obtain it, we return -1. + * @return The status code of the call + */ + public abstract int getStatusCode(); + + /** + * {@inheritDoc} + */ + @Override + public Map> getHeaderMap() { + return (Map>) getData(HEADER_MAP.name()); + } + + /** + * Retrieve the request ID of the downstream call + * @return the request ID + */ + public String getRequestId() { + return (String) getData(DataKey.REQUEST_ID.name()); + } + + /** + * Retrieve the number of times the downstream call had retried. + * @return The retry count. + */ + public int getRetryCount() { + Object retryCountObj = getData(DataKey.RETRIES.name()); + if (retryCountObj == null) { + return 0; // Assume if we've never set it, then we've never retried. + } + + return (int) retryCountObj; + } +} diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts b/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts index e01f9a3..da1da79 100644 --- a/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts @@ -20,11 +20,50 @@ plugins { tasks.shadowJar { manifest { attributes(mapOf( - "Disco-Installable-Classes" to "software.amazon.disco.agent.web.AWSSupport" + "Disco-Installable-Classes" to "software.amazon.disco.agent.AWSSupport" )) } } +// Defines a new source set for our safety integration test, which verifies that adding the Disco AWS Plugin +// will not break customer's code, even if they don't depend on the AWS SDK +sourceSets { + create("safetyTest") { + } +} + +// Initializes the safetyTestImplementation configuration needed to declare dependencies +val safetyTestImplementation by configurations.getting { + extendsFrom(configurations.implementation.get()) +} + +// Only dependency the test needs is JUnit +dependencies { + safetyTestImplementation("junit:junit:4.12") +} + +// This task adds the Disco Java Agent and AWS SDK plugin like a normal integ test, +// then runs the test(s) in the safetyTest/ source directory +// Adapted from: https://docs.gradle.org/current/userguide/java_testing.html#sec:configuring_java_integration_tests +val safetyTestTask = task("safetyTest") { + description = "Runs class safety tests" + group = "verification" + + testClassesDirs = sourceSets["safetyTest"].output.classesDirs + classpath = sourceSets["safetyTest"].runtimeClasspath + + jvmArgs("-javaagent:../../disco-java-agent/disco-java-agent/build/libs/disco-java-agent-${project.version}.jar=pluginPath=./build/libs") + + dependsOn(":disco-java-agent:disco-java-agent:build") + dependsOn(":disco-java-agent-aws:disco-java-agent-aws-plugin:assemble") + + mustRunAfter("test") +} + +tasks.check { + dependsOn(safetyTestTask) +} + configure { publications { named("maven") { diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/src/safetyTest/java/software/amazon/disco/agent/safety/AWSLibrariesAbsentTests.java b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/safetyTest/java/software/amazon/disco/agent/safety/AWSLibrariesAbsentTests.java new file mode 100644 index 0000000..cc7ec0d --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/safetyTest/java/software/amazon/disco/agent/safety/AWSLibrariesAbsentTests.java @@ -0,0 +1,11 @@ +package software.amazon.disco.agent.safety; + +import org.junit.Test; + +public class AWSLibrariesAbsentTests { + + @Test(expected = ClassNotFoundException.class) + public void testSDKCoreLibraryNotPresent() throws ClassNotFoundException { + Class.forName("software.amazon.awssdk.core.client.builder.SdkClientBuilder"); + } +} \ No newline at end of file diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/.gitkeep b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/.gitkeep deleted file mode 100644 index 3b67524..0000000 --- a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -TODO Add source code \ No newline at end of file diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/AWSSupport.java b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/AWSSupport.java new file mode 100644 index 0000000..de20029 --- /dev/null +++ b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/AWSSupport.java @@ -0,0 +1,25 @@ +package software.amazon.disco.agent; + +import software.amazon.disco.agent.awsv1.AWSClientInvokeInterceptor; +import software.amazon.disco.agent.awsv2.AWSClientBuilderInterceptor; +import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.agent.interception.Package; + +import java.util.Arrays; +import java.util.Collection; + +/** + * Bundle of AWS Support interceptors, for Agents to install as a whole. + */ +public class AWSSupport implements Package { + /** + * {@inheritDoc} + */ + @Override + public Collection get() { + return Arrays.asList( + new AWSClientInvokeInterceptor(), + new AWSClientBuilderInterceptor() + ); + } +} diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv1/AWSClientInvokeInterceptor.java b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv1/AWSClientInvokeInterceptor.java new file mode 100644 index 0000000..3303681 --- /dev/null +++ b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv1/AWSClientInvokeInterceptor.java @@ -0,0 +1,138 @@ +package software.amazon.disco.agent.awsv1; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import software.amazon.disco.agent.event.AwsV1ServiceDownstreamRequestEventImpl; +import software.amazon.disco.agent.event.EventBus; +import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; +import software.amazon.disco.agent.event.ServiceResponseEvent; +import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.agent.logging.LogManager; +import software.amazon.disco.agent.logging.Logger; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +/** + * When making a downstream AWS call, the doInvoke method is intercepted. + */ +public class AWSClientInvokeInterceptor implements Installable { + /** + * Disco logger. Must be public due to use in Advice methods + */ + public static final Logger log = LogManager.getLogger(AWSClientInvokeInterceptor.class); + + /** + * The Disco Origin for AWS SDK V1 events. Public because it's referenced in the Advice methods + */ + public static final String AWS_V1_ORIGIN = "AWSv1"; + + /** + * This method is inlined by ByteBuddy before the doInvoke() method inside each of the AWS clients. + * It reflectively acquires the service and operation names for the request, taking care to swallow and log + * any reflective exceptions. It uses the collected data to create a {@link ServiceDownstreamRequestEvent} + * and publish it to the Disco event bus. + * + * @param request - The method signature of the intercepted call is: + * Response (DefaultRequest, HttpResponseHandler, ExecutionContext + * The first argument is the request object which is the argument we need. + * @param origin - an identifier of the intercepted Method, for logging/debugging + * @return - The Disco event created + */ + @Advice.OnMethodEnter + public static ServiceDownstreamRequestEvent enter(@Advice.Argument(0) final Object request, + @Advice.Origin final String origin) + { + if (LogManager.isDebugEnabled()) { + log.debug("DiSCo(AWSv1) method interception of " + origin); + } + + String service = null; + String operation = null; + + //Retrieve name of AWS service we are calling + try { + service = (String) request.getClass().getMethod("getServiceName").invoke(request); + } catch (Exception e) { + log.warn("Disco(AWSv1) failed to retrieve service name from AWS client request", e); + } + + + //Original request contains both the operation name and the request object + try { + Object originalRequest = request.getClass().getMethod("getOriginalRequest").invoke(request); + operation = originalRequest.getClass().getSimpleName().replace("Request", ""); + } catch (Exception e) { + log.warn("Disco(AWSv1) failed to retrieve operation name from AWS client request", e); + } + + ServiceDownstreamRequestEvent requestEvent = new AwsV1ServiceDownstreamRequestEventImpl(AWS_V1_ORIGIN, service, operation); + requestEvent.withRequest(request); + EventBus.publish(requestEvent); + + return requestEvent; + } + + /** + * This method is inlined by ByteBuddy at the end of the doInvoke() method of all AWS clients. + * It constructs a {@link ServiceDownstreamResponseEvent} with the response of doInvoke(), metadata from the request + * event, and any throwable from the request and publishes it to the Disco Event Bus. + * + * @param requestEvent - The event produced by the {@link #enter} method + * @param response - The response returned from the AWS Client's request + * @param thrown - The throwable thrown by the request, if any + */ + @Advice.OnMethodExit(onThrowable = Throwable.class) + public static void exit(@Advice.Enter final ServiceDownstreamRequestEvent requestEvent, + @Advice.Return final Object response, + @Advice.Thrown final Throwable thrown) + { + + ServiceResponseEvent responseEvent = new ServiceDownstreamResponseEvent( + AWS_V1_ORIGIN, + requestEvent.getService(), + requestEvent.getOperation(), + requestEvent) + .withResponse(response) + .withThrown(thrown); + + EventBus.publish(responseEvent); + } + + /** + * {@inheritDoc} + */ + @Override + public AgentBuilder install(AgentBuilder agentBuilder) { + return agentBuilder + .type(buildClassMatcher()) + .transform(new AgentBuilder.Transformer.ForAdvice() + .advice(buildMethodMatcher(), AWSClientInvokeInterceptor.class.getName())); + } + + /** + * Builds a class matcher to discover all implemented AWS clients. + * @return an ElementMatcher suitable for passing to the type() method of a AgentBuilder + */ + ElementMatcher buildClassMatcher() { + return hasSuperType(named("com.amazonaws.AmazonWebServiceClient")) + .and(not(isInterface())) + .and(not(isAbstract())); + } + + /** + * Builds a method matcher to match against all doInvoke methods in AWS clients + * @return an ElementMatcher suitable for passing to builder.method() + */ + ElementMatcher buildMethodMatcher() { + return named("doInvoke") + .and(not(isAbstract())); + } +} \ No newline at end of file diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/AWSClientBuilderInterceptor.java b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/AWSClientBuilderInterceptor.java new file mode 100644 index 0000000..e008db2 --- /dev/null +++ b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/AWSClientBuilderInterceptor.java @@ -0,0 +1,100 @@ +package software.amazon.disco.agent.awsv2; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import software.amazon.awssdk.core.client.builder.SdkClientBuilder; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.disco.agent.interception.Installable; +import software.amazon.disco.agent.logging.LogManager; +import software.amazon.disco.agent.logging.Logger; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isFinal; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +public class AWSClientBuilderInterceptor implements Installable { + /** + * Disco logger. Must be public for use in Advice methods. + */ + public final static Logger log = LogManager.getLogger(AWSClientBuilderInterceptor.class); + + /** + * {@inheritDoc} + */ + @Override + public AgentBuilder install(AgentBuilder agentBuilder) { + return agentBuilder + .type(buildClassMatcher()) + .transform((builder, typeDescription, classLoader, module) -> + builder.method(buildMethodMatcher()).intercept(Advice.to(AWSClientBuilderInterceptorMethodDelegation.class))); + } + + /** + * Nested delegation class that handles any methods intercepted by the installable. Separate class is necessary + * so as to not load any AWS SDK V2 classes referenced within eagerly, which would cause ClassNotFoundException + * for Disco customers using this installable without using the AWS SDK V2. + */ + public static class AWSClientBuilderInterceptorMethodDelegation { + + /** + * The SdkClientBuilder#build method is intercepted, and this method is inlined in front of it. + * This interception occurs once per client created, e.g. it does not need any re-interception + * for each request. + * + * Once intercepted, the original Client Builder is modified to include the DiscoExecutionInterceptor in its + * execution chain. The DiscoExecutionInterceptor obtains relevant metadata during a + * client request invocation and publishes it into the AwsServiceDownstreamEvents through the EventBus. + * + * @param invoker the object that invoked the build method originally + * @param origin identifier of the intercepted method, for debugging/logging + */ + @Advice.OnMethodEnter + public static void enter(@Advice.This final SdkClientBuilder invoker, + @Advice.Origin final String origin) + { + if (LogManager.isDebugEnabled()) { + log.debug("DiSCo(AWSv2) method interception of " + origin); + } + + try { + ClientOverrideConfiguration configuration = ClientOverrideConfiguration + .builder() + .addExecutionInterceptor(new DiscoExecutionInterceptor()) + .build(); + invoker.overrideConfiguration(configuration); + } catch (Throwable t) { + log.error("Disco(AWSv2) Failed to add Execution Interceptor to client " + origin, t); + } + } + } + + /** + * Build a ElementMatcher which defines the kind of class which will be intercepted. Package-private for tests. + * + * @return A ElementMatcher suitable to pass to the type() method of an AgentBuilder + */ + static ElementMatcher buildClassMatcher() { + return hasSuperType(named("software.amazon.awssdk.core.client.builder.SdkClientBuilder")) + .and(not(isInterface())); + } + + /** + * Build an ElementMatcher which will match against the build() method of an Sdk Client Builder. + * Package-private for tests + * + * @return An ElementMatcher suitable for passing to the method() method of a DynamicType.Builder + */ + static ElementMatcher buildMethodMatcher() { + return named("build") + .and(takesArguments(0)) + .and(isFinal()) + .and(not(isAbstract())); + } +} diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptor.java b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptor.java new file mode 100644 index 0000000..6bb1079 --- /dev/null +++ b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptor.java @@ -0,0 +1,210 @@ +package software.amazon.disco.agent.awsv2; + +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttribute; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.disco.agent.event.AwsServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.AwsServiceDownstreamRequestEventImpl; +import software.amazon.disco.agent.event.AwsServiceDownstreamResponseEvent; +import software.amazon.disco.agent.event.AwsServiceDownstreamResponseEventImpl; +import software.amazon.disco.agent.logging.LogManager; +import software.amazon.disco.agent.logging.Logger; +import software.amazon.disco.agent.reflect.concurrent.TransactionContext; +import software.amazon.disco.agent.reflect.event.EventBus; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Implementation of the ExecutionInterceptor interface provided by the AWS SDK, which is the officially + * recommended way of intercepting and manipulating AWS SDK requests. In our implementation, we publish Disco + * Events before and after the execution, including if there was an error. We also use provided hooks to allow + * Disco consumers to manipulate request headers, and add metadata to events like number of retries. + * + * On a successful call the ordering of method calls is: + * beforeExecution to modifyHttpRequest to beforeTransmission to afterExecution + * + * On an unsuccessful execution, the ordering method calls is: + * beforeExecution - ... -- onExecutionFailure + * Meaning that it could fail on any portion of the call chain. + * + */ +public class DiscoExecutionInterceptor implements ExecutionInterceptor { + private static final Logger log = LogManager.getLogger(DiscoExecutionInterceptor.class); + + /** + * Common header keys to retrieve the request Id + */ + private static final List REQUEST_ID_KEYS = Arrays.asList("x-amz-request-id", "x-amzn-requestid"); + + /** + * Disco Origin. Visible for testing. + */ + static final String AWS_SDK_V2_CLIENT_ORIGIN = "AWSv2"; + + /** + * The AWS Region Execution Attribute, used to look up what region this request is being made in. We acquire it + * reflectively because it's defined in the "aws-core" library, which we don't compile against because it's + * possible that customers could be using this class without having "aws-core" on their classpath. + */ + private static ExecutionAttribute regionExecutionAttribute; + + /** + * Transaction Context keys to retrieve the request event and retry counts since these events could happen + * between different threads on the same transaction. + * Visible for testing + */ + static final String TX_REQUEST_EVENT_KEY = "AWSv2RequestEvent"; + static final String TX_RETRY_COUNT_KEY = "AWSv2RetryCount"; + + static { + try { + Class clazz = Class.forName("software.amazon.awssdk.awscore.AwsExecutionAttribute", true, ClassLoader.getSystemClassLoader()); + Field field = clazz.getField("AWS_REGION"); + regionExecutionAttribute = (ExecutionAttribute) field.get(null); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | ClassCastException e) { + log.debug("Failed to access AwsExecutionAttribute.AWS_REGION field, region will not be recorded in AwsServiceEvents"); + } + } + + /** + * The first API to be called by the execution interceptor chain. This is called before the request is modified by + * other interceptors. + * + * @param context The Context object passed in by the execution interceptor. + * This changes as we progress through different method calls. + * @param executionAttributes The execution attributes which contain information such as region, service name, etc. + */ + @Override + public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes executionAttributes) { + // getAttribute returns an arbitrary object. For the service name and operation name, they are returned as Strings. + String serviceName = executionAttributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME); + String operationName = executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME); + + // For the region, getAttribute returns a Region object, hence why we need to call toString(). + String region = null; + if (regionExecutionAttribute != null) { + region = executionAttributes.getAttribute(regionExecutionAttribute).toString(); + } + + AwsServiceDownstreamRequestEvent awsEvent = new AwsServiceDownstreamRequestEventImpl(AWS_SDK_V2_CLIENT_ORIGIN, serviceName, operationName) + .withRegion(region); + + awsEvent.withRequest(context.request()); + TransactionContext.putMetadata(TX_REQUEST_EVENT_KEY, awsEvent); + } + + /** + * This modifies the Http request object before it is transmitted. The modified Http request must be returned + * + * @param context The Context object passed in by the execution interceptor. + * This changes as we progress through different method calls. + * @param executionAttributes The execution attributes which contain information such as region, service name, etc. + * @return the modified Http request + */ + @Override + public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + AwsServiceDownstreamRequestEventImpl requestEvent = (AwsServiceDownstreamRequestEventImpl) TransactionContext.getMetadata(TX_REQUEST_EVENT_KEY); + + requestEvent.withSdkHttpRequest(context.httpRequest()) + .withHeaderMap(context.httpRequest().headers()); + + // Modification may happen by consumers of the request event + EventBus.publish(requestEvent); + + return context.httpRequest(); + } + + /** + * This is called before every API attempt. We use this to keep track of how many API attempts are made + * @param context The Context object passed in by the execution interceptor. + * This changes as we progress through different method calls. + * @param executionAttributes The execution attributes which contain information such as region, service name, etc. + */ + @Override + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + Object retryCountObj = TransactionContext.getMetadata(TX_RETRY_COUNT_KEY); + int retryCount; + if (retryCountObj == null) { + retryCount = 0; + } else { + retryCount = (int) retryCountObj + 1; + } + + TransactionContext.putMetadata(TX_RETRY_COUNT_KEY, retryCount); + } + + /** + * This is called after it has been potentially modified by other request interceptors before it is sent to the service. + * @param context The Context object passed in by the execution interceptor. + * This changes as we progress through different method calls. + * @param executionAttributes The execution attributes which contain information such as region, service name, etc. + */ + @Override + public void afterExecution(Context.AfterExecution context, ExecutionAttributes executionAttributes) { + AwsServiceDownstreamRequestEvent requestEvent = (AwsServiceDownstreamRequestEvent) TransactionContext.getMetadata(TX_REQUEST_EVENT_KEY); + + Object txRetryCount = TransactionContext.getMetadata(TX_RETRY_COUNT_KEY); + int retryCount = txRetryCount == null ? 0 : (int) txRetryCount; + + SdkHttpResponse httpResponse = context.httpResponse(); + AwsServiceDownstreamResponseEvent awsEvent = new AwsServiceDownstreamResponseEventImpl(requestEvent) + .withSdkHttpResponse(httpResponse) + .withHeaderMap(httpResponse.headers()) + .withRequestId(extractRequestId(httpResponse)) + .withRetryCount(retryCount); + + // Populate AWS SDK response + awsEvent.withResponse(context.response()); + EventBus.publish(awsEvent); + } + + /** + * This is called on failure during any point of the lifecycle of the request. + * @param context The Context object passed in by the execution interceptor. + * This changes as we progress through different method calls. + * At this point, it's a failed execution context object. + * @param executionAttributes The execution attributes which contain information such as region, service name, etc. + */ + @Override + public void onExecutionFailure(Context.FailedExecution context, ExecutionAttributes executionAttributes) { + AwsServiceDownstreamRequestEvent requestEvent = (AwsServiceDownstreamRequestEvent) TransactionContext.getMetadata(TX_REQUEST_EVENT_KEY); + + Object txRetryCount = TransactionContext.getMetadata(TX_RETRY_COUNT_KEY); + int retryCount = txRetryCount == null ? 0 : (int) txRetryCount; + + AwsServiceDownstreamResponseEvent awsEvent = new AwsServiceDownstreamResponseEventImpl(requestEvent) + .withSdkHttpResponse(context.httpResponse().orElse(null)) + .withHeaderMap(context.httpResponse().map(SdkHttpResponse::headers).orElse(CollectionUtils.unmodifiableMapOfLists(new HashMap<>()))) + .withRequestId(context.httpResponse().map(this::extractRequestId).orElse(null)) + .withRetryCount(retryCount); + awsEvent.withThrown(context.exception()); + EventBus.publish(awsEvent); + } + + /** + * Helper method for extracting the request ID from the HTTP Response. + * @param httpResponse The HTTP Response object with headers which contain the request ID + * @return The request ID or null if we failed to find it. + */ + private String extractRequestId(SdkHttpResponse httpResponse) { + Map> headerMap = httpResponse.headers(); + if (headerMap == null) return null; + + for(String request_id_key : REQUEST_ID_KEYS) { + List requestIdList = headerMap.get(request_id_key); + if (requestIdList != null && requestIdList.size() > 0) { + return requestIdList.get(0); // Arbitrarily get the first one since headers are one to many. + } + } + return null; + } +} diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEventImpl.java b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEventImpl.java new file mode 100644 index 0000000..6a38482 --- /dev/null +++ b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEventImpl.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.disco.agent.event; + +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.http.SdkHttpRequest; + +import java.util.List; +import java.util.Map; + +import static software.amazon.disco.agent.event.AwsServiceDownstreamEvent.DataKey.HEADER_MAP; + +/** + * Concrete implementation of the AwsServiceDownstreamRequestEvent. + */ +public class AwsServiceDownstreamRequestEventImpl extends AwsServiceDownstreamRequestEvent { + private SdkHttpRequest sdkHttpRequest; + + /** + * Construct a new AwsServiceDownstreamRequestEventImpl + * @param origin the origin of the downstream call e.g. 'AWSv2', 'AWS' + * @param service the service name e.g. 'DynamoDb' + * @param operation the operation name e.g. 'ListTables' + */ + public AwsServiceDownstreamRequestEventImpl(String origin, String service, String operation) { + super(origin, service, operation); + } + + /** + * Set the region for this event + * @param region the region + * @return 'this' for method chaining + */ + public AwsServiceDownstreamRequestEvent withRegion(String region) { + withData(DataKey.REGION.name(), region); + return this; + } + + /** + * Set the underlying sdkHttpRequest used for retrieving Http-specific metadata. + * @param sdkHttpRequest The sdkHttpRequest that is used to retrieve metadata + * @return 'this' for method chaining + */ + public AwsServiceDownstreamRequestEventImpl withSdkHttpRequest(SdkHttpRequest sdkHttpRequest) { + this.sdkHttpRequest = sdkHttpRequest; + return this; + } + + /** + * Set the underlying Header Map which contains the underlying Http Header that's going out to the service. + * @param headerMap The header map to store into the event. + * @return 'this' for method chaining + */ + public AwsServiceDownstreamRequestEventImpl withHeaderMap(Map> headerMap) { + this.withData(HEADER_MAP.name(), headerMap); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getValueForField(String fieldName, Class clazz) { + if (!(this.getRequest() instanceof SdkRequest)) return null; + + SdkRequest request = (SdkRequest) this.getRequest(); + return request.getValueForField(fieldName, clazz); + + } + + /** + * {@inheritDoc} + */ + @Override + public boolean replaceHeader(String name, String value) { + if (this.sdkHttpRequest == null) return false; + + this.sdkHttpRequest = sdkHttpRequest.toBuilder() + .appendHeader(name, value) + .build(); + + Map> headerMap = this.sdkHttpRequest.headers(); + this.withHeaderMap(headerMap); + return headerMap != null; + } +} diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamResponseEventImpl.java b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamResponseEventImpl.java new file mode 100644 index 0000000..77d317f --- /dev/null +++ b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamResponseEventImpl.java @@ -0,0 +1,103 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + + +package software.amazon.disco.agent.event; + +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.http.SdkHttpResponse; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static software.amazon.disco.agent.event.AwsServiceDownstreamEvent.DataKey.HEADER_MAP; + +public class AwsServiceDownstreamResponseEventImpl extends AwsServiceDownstreamResponseEvent { + private SdkHttpResponse sdkHttpResponse; + + /** + * Construct a new AwsServiceDownstreamRequestEventImpl + * @param requestEvent the associated request event + */ + public AwsServiceDownstreamResponseEventImpl(AwsServiceDownstreamRequestEvent requestEvent) { + super(requestEvent.getOrigin(), requestEvent.getService(), requestEvent.getOperation(), requestEvent); + } + + /** + * Set the SdkHttpResponse for the event. This is used to retrieve the Http specific metadata + * such as the header map and status code. + * @param sdkHttpResponse the SdkHttpResponse + * @return 'this' for method chaining + */ + public AwsServiceDownstreamResponseEventImpl withSdkHttpResponse(SdkHttpResponse sdkHttpResponse) { + this.sdkHttpResponse = sdkHttpResponse; + return this; + } + + /** + * Set the HTTP method in this event + * @param requestId the method e.g. GET, POST etc + * @return 'this' for method chaining + */ + public AwsServiceDownstreamResponseEventImpl withRequestId(String requestId) { + withData(DataKey.REQUEST_ID.name(), requestId); + return this; + } + + /** + * Set the underlying Header Map which contains the underlying Http Header that's going out to the service. + * + * @param headerMap The header map to add + * @return 'this' for method chaining + */ + public AwsServiceDownstreamResponseEventImpl withHeaderMap(Map> headerMap) { + this.withData(HEADER_MAP.name(), headerMap); + return this; + } + + /** + * Set the retry count for the event. If none is set, retrieval would default to 0. + * @param retryCount The amount of retries taken + * @return 'this' for method chaining + */ + public AwsServiceDownstreamResponseEventImpl withRetryCount(int retryCount) { + this.withData(DataKey.RETRIES.name(), retryCount); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public int getStatusCode() { + if (sdkHttpResponse == null) return -1; + + return sdkHttpResponse.statusCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getValueForField(String fieldName, Class clazz) { + if (!(this.getResponse() instanceof SdkResponse)) { + return Optional.empty(); // Response accessor could be null if the sdk call failed. + } + + SdkResponse response = (SdkResponse) this.getResponse(); + return response.getValueForField(fieldName, clazz); + } +} diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsV1ServiceDownstreamRequestEventImpl.java b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsV1ServiceDownstreamRequestEventImpl.java new file mode 100644 index 0000000..3c2b6fa --- /dev/null +++ b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsV1ServiceDownstreamRequestEventImpl.java @@ -0,0 +1,37 @@ +package software.amazon.disco.agent.event; + +import software.amazon.disco.agent.logging.LogManager; +import software.amazon.disco.agent.logging.Logger; + +import java.lang.reflect.Method; + +/** + * Extension of {@link ServiceDownstreamRequestEvent} that implements a AWS SDK specific replace header method. + */ +public class AwsV1ServiceDownstreamRequestEventImpl extends ServiceDownstreamRequestEvent implements HeaderReplaceable { + private static final Logger log = LogManager.getLogger(AwsV1ServiceDownstreamRequestEventImpl.class); + private static final String ADD_HEADER = "addHeader"; + + /** + * {@inheritDoc} + */ + public AwsV1ServiceDownstreamRequestEventImpl(String origin, String service, String operation) { + super(origin, service, operation); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean replaceHeader(String key, String value) { + Object awsSdkRequest = this.getRequest(); + try { + Method addHeader = awsSdkRequest.getClass().getDeclaredMethod(ADD_HEADER, String.class, String.class); + addHeader.invoke(awsSdkRequest, key, value); + return true; + } catch (Exception e) { + log.warn("Disco(AWSv1) Failed to add header '" + key + "' to AWS SDK Request", e); + return false; + } + } +} diff --git a/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/.gitkeep b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/.gitkeep deleted file mode 100644 index cc06484..0000000 --- a/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -TODO add unit tests \ No newline at end of file diff --git a/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/AWSSupportTests.java b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/AWSSupportTests.java new file mode 100644 index 0000000..ece18d0 --- /dev/null +++ b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/AWSSupportTests.java @@ -0,0 +1,19 @@ +package software.amazon.disco.agent; + +import org.junit.Assert; +import org.junit.Test; +import software.amazon.disco.agent.interception.Installable; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class AWSSupportTests { + @Test + public void testSqlSupport() { + Collection pkg = new AWSSupport().get(); + Set installables = new HashSet<>(); + installables.addAll(pkg); + Assert.assertEquals(2, installables.size()); + } +} diff --git a/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv1/AWSClientInvokeInterceptorTests.java b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv1/AWSClientInvokeInterceptorTests.java new file mode 100644 index 0000000..fbdd4d5 --- /dev/null +++ b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv1/AWSClientInvokeInterceptorTests.java @@ -0,0 +1,202 @@ +package software.amazon.disco.agent.awsv1; + +import com.amazonaws.AmazonWebServiceClient; +import com.amazonaws.ClientConfiguration; +import com.amazonaws.DefaultRequest; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.UpdateTableRequest; +import com.amazonaws.services.sns.AmazonSNSClient; +import com.amazonaws.services.sqs.AmazonSQSClient; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import software.amazon.awssdk.services.dynamodb.model.TableDescription; +import software.amazon.awssdk.services.dynamodb.model.UpdateTableResponse; +import software.amazon.disco.agent.event.Event; +import software.amazon.disco.agent.event.EventBus; +import software.amazon.disco.agent.event.Listener; +import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; + +import java.lang.reflect.Method; + +import static org.mockito.ArgumentMatchers.any; + +public class AWSClientInvokeInterceptorTests { + private static final String SERVICE = "DynamoDBv2"; + + private TestListener testListener; + private DefaultRequest request; + + @Before + public void before() { + EventBus.removeAllListeners(); + EventBus.addListener(testListener = new TestListener()); + request = new DefaultRequest( + new UpdateTableRequest("tableName", new ProvisionedThroughput(1L, 1L)), SERVICE); + } + + @After + public void after() { + EventBus.removeAllListeners(); + } + + @Test + public void testInstallation() { + AgentBuilder agentBuilder = Mockito.mock(AgentBuilder.class); + AgentBuilder.Identified.Extendable extendable = Mockito.mock(AgentBuilder.Identified.Extendable.class); + AgentBuilder.Identified.Narrowable narrowable = Mockito.mock(AgentBuilder.Identified.Narrowable.class); + AWSClientInvokeInterceptor interceptor = new AWSClientInvokeInterceptor(); + Mockito.when(agentBuilder.type(Mockito.any(ElementMatcher.class))).thenReturn(narrowable); + Mockito.when(narrowable.transform(any(AgentBuilder.Transformer.class))).thenReturn(extendable); + AgentBuilder result = interceptor.install(agentBuilder); + Assert.assertSame(extendable, result); + } + + @Test + public void testMethodMatcherSucceeds() throws Exception { + Assert.assertTrue(methodMatches("doInvoke", AmazonSQSClient.class)); + } + + @Test(expected = NoSuchMethodException.class) + public void testMethodMatcherFailsOnMethod() throws Exception { + methodMatches("notAMethod", AmazonDynamoDBClient.class); + } + + @Test(expected = NoSuchMethodException.class) + public void testMethodMatcherFailsOnClass() throws Exception { + Assert.assertFalse(methodMatches("doInvoke", String.class)); + } + + @Test + public void testClassMatcherSucceeds() { + Assert.assertTrue(classMatches(AmazonSNSClient.class)); + } + + @Test + public void testClassMatcherFails() { + Assert.assertFalse(classMatches(String.class)); + } + + @Test + public void testClassMatcherFailsOnAbstractType() { + Assert.assertFalse(classMatches(FakeAWSClass.class)); + } + + @Test + public void testRequestEventCreation() { + ServiceDownstreamRequestEvent event = AWSClientInvokeInterceptor.enter(request, null); + + Assert.assertEquals(AWSClientInvokeInterceptor.AWS_V1_ORIGIN, event.getOrigin()); + Assert.assertEquals(SERVICE, event.getService()); + Assert.assertEquals("UpdateTable", event.getOperation()); + } + + @Test + public void testRequestEventPublish() { + ServiceDownstreamRequestEvent event = AWSClientInvokeInterceptor.enter(request, null); + + Assert.assertNotNull(testListener.request); + Assert.assertEquals(event, testListener.request); + Assert.assertEquals(request, testListener.request.getRequest()); + } + + @Test + public void testResponseEventPublish() { + UpdateTableResponse response = UpdateTableResponse.builder().tableDescription((TableDescription) null).build(); + ServiceDownstreamRequestEvent requestEvent = AWSClientInvokeInterceptor.enter(request, null); + + AWSClientInvokeInterceptor.exit(requestEvent, response, null); + ServiceDownstreamResponseEvent responseEvent = testListener.response; + + Assert.assertNotNull(responseEvent); + Assert.assertEquals(AWSClientInvokeInterceptor.AWS_V1_ORIGIN, responseEvent.getOrigin()); + Assert.assertEquals(SERVICE, responseEvent.getService()); + Assert.assertEquals("UpdateTable", responseEvent.getOperation()); + Assert.assertEquals(response, responseEvent.getResponse()); + Assert.assertNull(responseEvent.getThrown()); + } + + @Test + public void testResponseEventWithThrowable() { + Exception thrown = new RuntimeException(); + ServiceDownstreamRequestEvent requestEvent = AWSClientInvokeInterceptor.enter(request, null); + + AWSClientInvokeInterceptor.exit(requestEvent, null, thrown); + + Assert.assertNotNull(testListener.response); + Assert.assertEquals(thrown, testListener.response.getThrown()); + Assert.assertNull(testListener.response.getResponse()); + } + + /** + * Helper function to test the class matcher matching + * @param clazz Class type we are validating + * @return true if matches else false + */ + private boolean classMatches(Class clazz) { + AWSClientInvokeInterceptor interceptor = new AWSClientInvokeInterceptor(){}; + return interceptor.buildClassMatcher().matches(new TypeDescription.ForLoadedType(clazz)); + } + + /** + * Helper function to test the method matcher against an input class + * @param methodName name of method + * @param paramType class we are verifying contains the method + * @return true if matches, else false + * @throws NoSuchMethodException + */ + private boolean methodMatches(String methodName, Class paramType) throws NoSuchMethodException { + Method method = null; + for (Method m: paramType.getDeclaredMethods()) { + if (m.getName().equals(methodName)) { + Assert.assertNull(method); + method = m; + } + } + + if (method == null) { + throw new NoSuchMethodException(); + } + + AWSClientInvokeInterceptor interceptor = new AWSClientInvokeInterceptor(); + return interceptor.buildMethodMatcher() + .matches(new MethodDescription.ForLoadedMethod(method)); + } + + private static class TestListener implements Listener { + ServiceDownstreamRequestEvent request; + ServiceDownstreamResponseEvent response; + @Override + public int getPriority() { + return 0; + } + + @Override + public void listen(Event e) { + if (e instanceof ServiceDownstreamRequestEvent) { + request = (ServiceDownstreamRequestEvent)e; + } else if (e instanceof ServiceDownstreamResponseEvent) { + response = (ServiceDownstreamResponseEvent)e; + } else { + Assert.fail("Unexpected event"); + } + } + } + + /** + * Fake AWS class to test that only non-abstract classes are instrumented + */ + public abstract class FakeAWSClass extends AmazonWebServiceClient { + public FakeAWSClass(ClientConfiguration clientConfiguration) { + super(clientConfiguration); + } + } +} diff --git a/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv2/AWSClientBuilderInterceptorTests.java b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv2/AWSClientBuilderInterceptorTests.java new file mode 100644 index 0000000..233255b --- /dev/null +++ b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv2/AWSClientBuilderInterceptorTests.java @@ -0,0 +1,126 @@ +package software.amazon.disco.agent.awsv2; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import software.amazon.awssdk.core.client.builder.SdkClientBuilder; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder; +import software.amazon.awssdk.utils.AttributeMap; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AWSClientBuilderInterceptorTests { + private AWSClientBuilderInterceptor interceptor; + + @Spy + SdkClientBuilder builderSpy; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + interceptor = new AWSClientBuilderInterceptor(); + } + + @Test + public void testInstallation() { + AgentBuilder agentBuilder = mock(AgentBuilder.class); + AgentBuilder.Identified.Extendable extendable = mock(AgentBuilder.Identified.Extendable.class); + AgentBuilder.Identified.Narrowable narrowable = mock(AgentBuilder.Identified.Narrowable.class); + when(agentBuilder.type(any(ElementMatcher.class))).thenReturn(narrowable); + when(narrowable.transform(any(AgentBuilder.Transformer.class))).thenReturn(extendable); + AgentBuilder result = interceptor.install(agentBuilder); + assertSame(extendable, result); + } + + @Test + public void testCorrectClassMatches() { + DynamoDbClientBuilder builder = DynamoDbClient.builder(); + Assert.assertTrue(classMatches(builder.getClass())); + } + + @Test + public void testIncorrectClassDoesNotMatch() { + Assert.assertFalse(classMatches(String.class)); + } + + @Test + public void testMethodMatches() throws NoSuchMethodException { + Assert.assertEquals(1, methodMatchedCount("build", MyBuilder.class)); + } + + @Test + public void testInterfaceMethodDoesNotMatch() throws NoSuchMethodException { + Assert.assertEquals(0, methodMatchedCount("build", SdkHttpClient.Builder.class)); + } + + @Test + public void testMethodEnter() { + AWSClientBuilderInterceptor.AWSClientBuilderInterceptorMethodDelegation.enter(builderSpy, "origin"); + + verify(builderSpy, times(1)).overrideConfiguration((ClientOverrideConfiguration) any()); + } + + private static boolean classMatches(Class clazz) { + return AWSClientBuilderInterceptor.buildClassMatcher().matches(new TypeDescription.ForLoadedType(clazz)); + } + + /** + * Helper method to test the method matcher against an input class + * + * @param methodName name of method + * @param paramType class we are verifying contains the method + * @return Matched methods count + * @throws NoSuchMethodException + */ + private int methodMatchedCount(String methodName, Class paramType) throws NoSuchMethodException { + List methods = new ArrayList<>(); + for (Method m : paramType.getDeclaredMethods()) { + if (m.getName().equals(methodName)) { + System.out.println(m); + methods.add(m); + } + } + + if (methods.size() == 0) throw new NoSuchMethodException(); + + int matchedCount = 0; + for (Method m : methods) { + MethodDescription.ForLoadedMethod forLoadedMethod = new MethodDescription.ForLoadedMethod(m); + if (AWSClientBuilderInterceptor.buildMethodMatcher().matches(forLoadedMethod)) { + matchedCount++; + } + } + return matchedCount; + } + + private static final class MyBuilder implements SdkHttpClient.Builder { + + @Override + public final SdkHttpClient build() { + return null; + } + + @Override + public SdkHttpClient buildWithDefaults(AttributeMap attributeMap) { + return null; + } + } +} diff --git a/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptorTests.java b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptorTests.java new file mode 100644 index 0000000..bedcdc6 --- /dev/null +++ b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptorTests.java @@ -0,0 +1,206 @@ +package software.amazon.disco.agent.awsv2; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import software.amazon.awssdk.awscore.AwsExecutionAttribute; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.InterceptorContext; +import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpFullResponse; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.regions.Region; +import software.amazon.disco.agent.concurrent.TransactionContext; +import software.amazon.disco.agent.event.AbstractServiceEvent; +import software.amazon.disco.agent.event.AwsServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.AwsServiceDownstreamResponseEvent; +import software.amazon.disco.agent.event.Event; +import software.amazon.disco.agent.event.EventBus; +import software.amazon.disco.agent.event.Listener; +import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.Mockito.when; + +public class DiscoExecutionInterceptorTests { + private static final String SERVICE = "service"; + private static final String OPERATION = "operation"; + private static final String REGION = "us-west-2"; + private static final int STATUS_CODE = 200; + private static final String REQUEST_ID = "12345"; + + private DiscoExecutionInterceptor interceptor; + private Map> headers; + private TestListener testListener; + private InterceptorContext context; + private ExecutionAttributes executionAttributes; + + @Mock + private SdkRequest sdkRequestMock; + + @Mock + private SdkHttpFullRequest sdkHttpRequestMock; + + @Mock + private SdkResponse sdkResponseMock; + + @Mock + private SdkHttpFullResponse sdkHttpResponseMock; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + + TransactionContext.clear(); + EventBus.removeAllListeners(); + EventBus.addListener(testListener = new TestListener()); + interceptor = new DiscoExecutionInterceptor(); + headers = new HashMap<>(); + context = InterceptorContext.builder() + .httpRequest(sdkHttpRequestMock) + .httpResponse(sdkHttpResponseMock) + .request(sdkRequestMock) + .response(sdkResponseMock) + .build(); + + executionAttributes = new ExecutionAttributes(); + executionAttributes.putAttribute(SdkExecutionAttribute.SERVICE_NAME, SERVICE); + executionAttributes.putAttribute(SdkExecutionAttribute.OPERATION_NAME, OPERATION); + executionAttributes.putAttribute(AwsExecutionAttribute.AWS_REGION, Region.of(REGION)); + + when(sdkHttpRequestMock.headers()).thenReturn(headers); + when(sdkHttpResponseMock.headers()).thenReturn(headers); + when(sdkHttpResponseMock.statusCode()).thenReturn(STATUS_CODE); + } + + @Test + public void testBeforeExecution() { + interceptor.beforeExecution(context, executionAttributes); + + AwsServiceDownstreamRequestEvent event = + (AwsServiceDownstreamRequestEvent) TransactionContext.getMetadata(DiscoExecutionInterceptor.TX_REQUEST_EVENT_KEY); + + verifyEvent(event); + Assert.assertNotNull(event); + Assert.assertEquals(REGION, event.getRegion()); + } + + @Test + public void testModifyHttpRequest() { + interceptor.beforeExecution(context, executionAttributes); // to populate TX + SdkHttpRequest sdkHttpRequest = interceptor.modifyHttpRequest(context, executionAttributes); + + Assert.assertEquals(sdkHttpRequestMock, sdkHttpRequest); + } + + @Test + public void testModifyHttpRequestPublishesRequestEvent() { + interceptor.beforeExecution(context, executionAttributes); // to populate TX + interceptor.modifyHttpRequest(context, executionAttributes); + + AwsServiceDownstreamRequestEvent event = testListener.request; + + verifyEvent(event); + Assert.assertEquals(REGION, event.getRegion()); + } + + @Test + public void testBeforeTransmission() { + interceptor.beforeTransmission(null, null); // First invocation initializes value + Assert.assertEquals(0, TransactionContext.getMetadata(DiscoExecutionInterceptor.TX_RETRY_COUNT_KEY)); + + interceptor.beforeTransmission(null, null); // Subsequent invokes increment it + Assert.assertEquals(1, TransactionContext.getMetadata(DiscoExecutionInterceptor.TX_RETRY_COUNT_KEY)); + } + + @Test + public void testAfterExecution() { + // Set up TX & request event + interceptor.beforeExecution(context, executionAttributes); + interceptor.modifyHttpRequest(context, executionAttributes); + + // Then call method-under-test + interceptor.afterExecution(context, executionAttributes); + + AwsServiceDownstreamResponseEvent event = testListener.response; + verifyEvent(event); + Assert.assertEquals(0, event.getRetryCount()); + Assert.assertEquals(STATUS_CODE, event.getStatusCode()); + Assert.assertEquals(sdkResponseMock, event.getResponse()); + Assert.assertEquals(testListener.request, event.getRequest()); + Assert.assertEquals(headers, event.getHeaderMap()); + Assert.assertNull(event.getThrown()); + } + + @Test + public void testAfterExecutionWithMultipleRetries() { + // Set up TX & request event + interceptor.beforeExecution(context, executionAttributes); + interceptor.modifyHttpRequest(context, executionAttributes); + + // "Mock" 2 retries + interceptor.beforeTransmission(null, null); + interceptor.beforeTransmission(null, null); + interceptor.beforeTransmission(null, null); + + // Then call method-under-test + interceptor.afterExecution(context, executionAttributes); + + verifyEvent(testListener.response); + Assert.assertEquals(2, testListener.response.getRetryCount()); + } + + @Test + public void testAfterExecutionRecordsRequestId() { + List requestIdList = new ArrayList<>(); + requestIdList.add(REQUEST_ID); + headers.put("x-amzn-requestid", requestIdList); + + // Set up TX & request event + interceptor.beforeExecution(context, executionAttributes); + interceptor.modifyHttpRequest(context, executionAttributes); + + // Then call method-under-test + interceptor.afterExecution(context, executionAttributes); + + verifyEvent(testListener.response); + Assert.assertEquals(REQUEST_ID, testListener.response.getRequestId()); + } + + private void verifyEvent(AbstractServiceEvent event) { + Assert.assertNotNull(event); + Assert.assertEquals(DiscoExecutionInterceptor.AWS_SDK_V2_CLIENT_ORIGIN, event.getOrigin()); + Assert.assertEquals(SERVICE, event.getService()); + Assert.assertEquals(OPERATION, event.getOperation()); + } + + private static class TestListener implements Listener { + AwsServiceDownstreamRequestEvent request; + AwsServiceDownstreamResponseEvent response; + @Override + public int getPriority() { + return 0; + } + + @Override + public void listen(Event e) { + if (e instanceof ServiceDownstreamRequestEvent) { + request = (AwsServiceDownstreamRequestEvent) e; + } else if (e instanceof ServiceDownstreamResponseEvent) { + response = (AwsServiceDownstreamResponseEvent) e; + } else { + Assert.fail("Unexpected event"); + } + } + } +} diff --git a/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/event/AwsServiceDownstreamEventTests.java b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/event/AwsServiceDownstreamEventTests.java new file mode 100644 index 0000000..6ac4c3b --- /dev/null +++ b/disco-java-agent-aws/src/test/java/software/amazon/disco/agent/event/AwsServiceDownstreamEventTests.java @@ -0,0 +1,124 @@ +package software.amazon.disco.agent.event; + +import com.amazonaws.DefaultRequest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import software.amazon.awssdk.http.SdkHttpFullRequest; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +public class AwsServiceDownstreamEventTests { + private static final String ORIGIN = "origin"; + private static final String SERVICE = "service"; + private static final String OPERATION = "operation"; + private static final String REGION = "region"; + private static final String REQUEST_ID = "123456789"; + private static final String HEADER_KEY = "some-header"; + private static final String HEADER_VALUE = "some-value"; + + private Map> headerMap; + + @Mock + SdkHttpFullRequest httpRequestMock; + + @Mock + SdkHttpFullRequest.Builder httpRequestBuilderMock; + + @Before + public void setup() { + headerMap = new HashMap<>(); + + MockitoAnnotations.openMocks(this); + when(httpRequestMock.toBuilder()).thenReturn(httpRequestBuilderMock); + when(httpRequestBuilderMock.build()).thenReturn(httpRequestMock); + when(httpRequestBuilderMock.appendHeader(anyString(), anyString())).thenAnswer(new AppendHeader(headerMap)); + when(httpRequestMock.headers()).thenReturn(headerMap); + } + + @Test + public void testAwsServiceDownstreamRequestEvent() { + AwsServiceDownstreamRequestEvent requestEvent = new AwsServiceDownstreamRequestEventImpl(ORIGIN, SERVICE, OPERATION) + .withRegion(REGION); + + verifyEvent(requestEvent); + Assert.assertEquals(REGION, requestEvent.getRegion()); + } + + @Test + public void testAwsServiceDownstreamResponseEvent() { + AwsServiceDownstreamRequestEvent requestEvent = new AwsServiceDownstreamRequestEventImpl(ORIGIN, SERVICE, OPERATION) + .withRegion(REGION); + AwsServiceDownstreamResponseEvent responseEvent = new AwsServiceDownstreamResponseEventImpl(requestEvent) + .withRetryCount(5) + .withRequestId(REQUEST_ID); + + verifyEvent(responseEvent); + Assert.assertEquals(5, responseEvent.getRetryCount()); + Assert.assertEquals(REQUEST_ID, responseEvent.getRequestId()); + } + + @Test + public void testReplaceHeaderInV1Request() { + DefaultRequest request = new DefaultRequest("my_service"); + Assert.assertTrue(request.getHeaders().isEmpty()); + + ServiceDownstreamRequestEvent event = new AwsV1ServiceDownstreamRequestEventImpl(ORIGIN, SERVICE, OPERATION); + event.withRequest(request); + + Assert.assertTrue(event instanceof HeaderReplaceable); + HeaderReplaceable replaceable = (HeaderReplaceable) event; + replaceable.replaceHeader(HEADER_KEY, HEADER_VALUE); + + Map headers = request.getHeaders(); + Assert.assertEquals(1, headers.size()); + Assert.assertEquals(HEADER_VALUE, headers.get(HEADER_KEY)); + } + + @Test + public void testReplaceHeaderInV2Request() { + AwsServiceDownstreamRequestEventImpl requestEvent = new AwsServiceDownstreamRequestEventImpl(ORIGIN, SERVICE, OPERATION) + .withHeaderMap(headerMap) + .withSdkHttpRequest(httpRequestMock); + + Assert.assertTrue(requestEvent instanceof HeaderReplaceable); + requestEvent.replaceHeader(HEADER_KEY, HEADER_VALUE); + + Assert.assertEquals(headerMap, requestEvent.getHeaderMap()); + Assert.assertEquals(1, headerMap.size()); + Assert.assertEquals(1, headerMap.get(HEADER_KEY).size()); + Assert.assertEquals(HEADER_VALUE, headerMap.get(HEADER_KEY).get(0)); + } + + private void verifyEvent(ServiceEvent event) { + Assert.assertEquals(ORIGIN, event.getOrigin()); + Assert.assertEquals(SERVICE, event.getService()); + Assert.assertEquals(OPERATION, event.getOperation()); + } + + private static class AppendHeader implements Answer { + public final Map> headers; + + public AppendHeader(Map headers) { + this.headers = headers; + } + + @Override + public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + List valueList = new ArrayList<>(); + valueList.add(invocationOnMock.getArgument(1)); + headers.put(invocationOnMock.getArgument(0), valueList); + return invocationOnMock.getMock(); + } + } +} From 6361ca12f828341362c2cb2c850e3ea6bcbdbc9e Mon Sep 17 00:00:00 2001 From: William Armiros Date: Thu, 20 Aug 2020 14:09:18 -0700 Subject: [PATCH 44/45] Added aws interception integ tests for both versions of aws sdk --- .../event/AwsServiceDownstreamEvent.java | 2 +- .../AwsServiceDownstreamRequestEvent.java | 15 - .../build.gradle.kts | 5 + .../java/software/amazon/disco/agent/.gitkeep | 1 - .../awsv1/AWSV1InterceptionTests.java | 221 ++++++++++++ .../awsv2/AWSV2InterceptionTests.java | 326 ++++++++++++++++++ .../amazon/disco/agent/integtest/s3File.txt | 1 + .../awsv2/DiscoExecutionInterceptor.java | 10 +- .../AwsServiceDownstreamRequestEventImpl.java | 12 +- 9 files changed, 570 insertions(+), 23 deletions(-) delete mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/.gitkeep create mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/integtest/awsv1/AWSV1InterceptionTests.java create mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/integtest/awsv2/AWSV2InterceptionTests.java create mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/resources/software/amazon/disco/agent/integtest/s3File.txt diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamEvent.java b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamEvent.java index 8893b90..17bc7fb 100644 --- a/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamEvent.java +++ b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamEvent.java @@ -22,7 +22,7 @@ * Generic interface of the Aws Service Downstream Events. Unifies common APIs that exist in both the request * and response events. */ -public interface AwsServiceDownstreamEvent { +public interface AwsServiceDownstreamEvent extends ServiceEvent { /** * Keys to use in the data map diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEvent.java b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEvent.java index e34b60d..c126525 100644 --- a/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEvent.java +++ b/disco-java-agent-aws/disco-java-agent-aws-api/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEvent.java @@ -29,25 +29,10 @@ public abstract class AwsServiceDownstreamRequestEvent extends ServiceDownstream * Keys to use in the data map */ enum DataKey { - /** - * The Request Id of the current sdk request. - */ - REQUEST_ID, - /** * The region of the request */ REGION, - - /** - * The operation name of the current request - */ - OPERATION_NAME, - - /** - * The service that the request is going to - */ - SERVICE_NAME, } /** diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts b/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts index da1da79..77d7c9f 100644 --- a/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts @@ -39,6 +39,11 @@ val safetyTestImplementation by configurations.getting { // Only dependency the test needs is JUnit dependencies { + testImplementation("com.amazonaws", "aws-java-sdk-dynamodb", "1.11.840") + testImplementation("software.amazon.awssdk", "dynamodb", "2.13.76") + testImplementation("software.amazon.awssdk", "s3", "2.13.76") + testImplementation("com.github.tomakehurst", "wiremock-jre8", "2.27.0") + testImplementation(project(":disco-java-agent-aws:disco-java-agent-aws-api")) safetyTestImplementation("junit:junit:4.12") } diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/.gitkeep b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/.gitkeep deleted file mode 100644 index bc9b219..0000000 --- a/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -TODO: Replace with integ tests \ No newline at end of file diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/integtest/awsv1/AWSV1InterceptionTests.java b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/integtest/awsv1/AWSV1InterceptionTests.java new file mode 100644 index 0000000..f9eaead --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/integtest/awsv1/AWSV1InterceptionTests.java @@ -0,0 +1,221 @@ +package software.amazon.disco.agent.integtest.awsv1; + +import com.amazonaws.ClientConfiguration; +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.SdkClientException; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; +import com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException; +import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; +import com.amazonaws.services.dynamodbv2.model.ListTablesRequest; +import com.amazonaws.services.dynamodbv2.model.ListTablesResult; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.disco.agent.event.Event; +import software.amazon.disco.agent.event.HeaderReplaceable; +import software.amazon.disco.agent.event.Listener; +import software.amazon.disco.agent.event.ServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.ServiceDownstreamResponseEvent; +import software.amazon.disco.agent.event.ServiceEvent; +import software.amazon.disco.agent.reflect.event.EventBus; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; + +public class AWSV1InterceptionTests { + private static final int PORT = 8089; + private static final String ENDPOINT = "http://127.0.0.1:" + PORT; + private static final String REGION = "us-west-2"; + private static final String HEADER_KEY = "someKey"; + private static final String HEADER_VAL = "someVal"; + + private TestListener testListener; + + @Rule + public WireMockRule wireMockRule = new WireMockRule(PORT); + + /** + * API info for DDB requests from: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_ListTables.html + */ + @Before + public void setup() { + testListener = new TestListener(); + EventBus.addListener(testListener); + + // Stub out fake response for ListTables, the "default" operation for these tests + String result = "{\"TableNames\":[\"ATestTable\",\"dynamodb-user\",\"scorekeep-state\",\"scorekeep-user\"]}"; + stubFor(post(urlEqualTo("/")) + .withHeader("X-Amz-Target", containing("ListTables")) + .willReturn(aResponse() + .withStatus(200) + .withBody(result))); + } + + @After + public void cleanup() { + EventBus.removeAllListeners(); + } + + @Test + public void testNormalRequestIntercepted() { + AmazonDynamoDB client = (AmazonDynamoDB) getTestableClient(AmazonDynamoDBClientBuilder.standard()).build(); + ListTablesRequest listTablesRequest = new ListTablesRequest(); + client.listTables(listTablesRequest); + + // Verify the HTTP request to AWS was made + verify(postRequestedFor(urlEqualTo("/")).withHeader("X-Amz-Target", containing("ListTables"))); + + Assert.assertNotNull(testListener.requestEvent); + verifyEvent(testListener.requestEvent, "AmazonDynamoDBv2", "ListTables"); + Assert.assertTrue(testListener.requestEvent.getRequest() instanceof Request); + + Assert.assertNotNull(testListener.responseEvent); + verifyEvent(testListener.responseEvent, "AmazonDynamoDBv2", "ListTables"); + Assert.assertTrue(testListener.responseEvent.getResponse() instanceof Response); + Assert.assertNull(testListener.responseEvent.getThrown()); + } + + // TODO: Add integration test for S3 once supported + + @Test + public void testReplaceHeaders() { + EventBus.addListener(new HeaderListener()); + + AmazonDynamoDB client = (AmazonDynamoDB) getTestableClient(AmazonDynamoDBClientBuilder.standard()).build(); + client.listTables(); + + // Verify the HTTP request to AWS was made with our injected header + verify(postRequestedFor(urlEqualTo("/")) + .withHeader("X-Amz-Target", containing("ListTables")) + .withHeader(HEADER_KEY, equalTo(HEADER_VAL))); + } + + @Test + public void testAwsServiceException() { + stubFor(post(urlEqualTo("/")) + .withHeader("X-Amz-Target", containing("DescribeTable")) + .willReturn(aResponse() + .withStatus(500) + .withBody("A service error occurred"))); + + AmazonDynamoDB client = (AmazonDynamoDB) getTestableClient(AmazonDynamoDBClientBuilder.standard()).build(); + DescribeTableRequest describeTableRequest = new DescribeTableRequest(); + AmazonDynamoDBException exception = null; + + try { + client.describeTable(describeTableRequest); + Assert.fail(); // Request should throw exception + } catch (AmazonDynamoDBException e) { + exception = e; + } + + // Verify the HTTP request to AWS was made + verify(postRequestedFor(urlEqualTo("/")).withHeader("X-Amz-Target", containing("DescribeTable"))); + + Assert.assertNotNull(testListener.requestEvent); + verifyEvent(testListener.requestEvent, "AmazonDynamoDBv2", "DescribeTable"); + Assert.assertTrue(testListener.requestEvent.getRequest() instanceof Request); + + Assert.assertNotNull(testListener.responseEvent); + verifyEvent(testListener.responseEvent, "AmazonDynamoDBv2", "DescribeTable"); + Assert.assertNull(testListener.responseEvent.getResponse()); + Assert.assertNotNull(exception); + Assert.assertEquals(exception, testListener.responseEvent.getThrown()); + } + + @Test + public void testAwsClientException() { + // Override endpoint from WireMock to fake endpoint, causing a SDK Client exception + AmazonDynamoDB client = (AmazonDynamoDB) getTestableClient(AmazonDynamoDBClientBuilder.standard()) + .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://some.nonexistent.endpoint.com/path/to/page", "us-west-2")) + .build(); + SdkClientException exception = null; + + try { + client.listTables(); + Assert.fail(); // SDK should throw connection refused exception + } catch (SdkClientException e) { + exception = e; + } + + Assert.assertNotNull(testListener.requestEvent); + verifyEvent(testListener.requestEvent, "AmazonDynamoDBv2", "ListTables"); + Assert.assertTrue(testListener.requestEvent.getRequest() instanceof Request); + + Assert.assertNotNull(testListener.responseEvent); + verifyEvent(testListener.responseEvent, "AmazonDynamoDBv2", "ListTables"); + Assert.assertNull(testListener.responseEvent.getResponse()); + Assert.assertNotNull(exception); + Assert.assertEquals(exception, testListener.responseEvent.getThrown()); + } + + private AwsClientBuilder getTestableClient(AwsClientBuilder builder) { + AWSCredentialsProvider fakeCredentials = new AWSStaticCredentialsProvider(new BasicAWSCredentials("fake", "fake")); + AwsClientBuilder.EndpointConfiguration mockEndpoint = new AwsClientBuilder.EndpointConfiguration(ENDPOINT, REGION); + ClientConfiguration clientConfiguration = new ClientConfiguration(); + clientConfiguration.setMaxErrorRetry(0); + clientConfiguration.setRequestTimeout(1000); + + return builder + .withEndpointConfiguration(mockEndpoint) + .withCredentials(fakeCredentials) + .withClientConfiguration(clientConfiguration); + } + + private void verifyEvent(ServiceEvent event, String service, String operation) { + Assert.assertEquals("AWSv1", event.getOrigin()); + Assert.assertEquals(service, event.getService()); + Assert.assertEquals(operation, event.getOperation()); + } + + private static class TestListener implements Listener { + ServiceDownstreamRequestEvent requestEvent; + ServiceDownstreamResponseEvent responseEvent; + + @Override + public int getPriority() { + return 0; + } + + @Override + public void listen(Event e) { + if (e instanceof ServiceDownstreamRequestEvent) { + requestEvent = (ServiceDownstreamRequestEvent) e; + } else if (e instanceof ServiceDownstreamResponseEvent) { + responseEvent = (ServiceDownstreamResponseEvent) e; + } else { + Assert.fail("Unexpected event"); + } + } + } + + private static class HeaderListener implements Listener { + + @Override + public int getPriority() { + return 1; + } + + @Override + public void listen(Event e) { + if (e instanceof HeaderReplaceable) { + ((HeaderReplaceable) e).replaceHeader(HEADER_KEY, HEADER_VAL); + } + } + } +} diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/integtest/awsv2/AWSV2InterceptionTests.java b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/integtest/awsv2/AWSV2InterceptionTests.java new file mode 100644 index 0000000..83236a8 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/java/software/amazon/disco/agent/integtest/awsv2/AWSV2InterceptionTests.java @@ -0,0 +1,326 @@ +package software.amazon.disco.agent.integtest.awsv2; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.client.builder.SdkClientBuilder; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder; +import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3ClientBuilder; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.disco.agent.event.AwsServiceDownstreamEvent; +import software.amazon.disco.agent.event.AwsServiceDownstreamRequestEvent; +import software.amazon.disco.agent.event.AwsServiceDownstreamResponseEvent; +import software.amazon.disco.agent.event.Event; +import software.amazon.disco.agent.event.HeaderReplaceable; +import software.amazon.disco.agent.event.Listener; +import software.amazon.disco.agent.reflect.concurrent.TransactionContext; +import software.amazon.disco.agent.reflect.event.EventBus; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.put; +import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; + +public class AWSV2InterceptionTests { + private static final int PORT = 8089; + private static final String ENDPOINT = "http://127.0.0.1:" + PORT; + private static final String REQUEST_ID = "12345"; + private static final String HEADER_KEY = "someKey"; + private static final String HEADER_VAL = "someVal"; + private static final String S3_BUCKET = "myBucket"; + private static final String S3_KEY = "myKey"; + + private TestListener testListener; + + private static final String LIST_TABLES = "{\"TableNames\":[\"ATestTable\",\"dynamodb-user\",\"scorekeep-state\",\"scorekeep-user\"]}"; + + /** + * API info from: + * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_ListTables.html + * https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html + */ + @Before + public void setup() { + testListener = new TestListener(); + EventBus.addListener(testListener); + + // Stub out fake response for ListTables, the "default" operation for these tests + stubFor(post(urlEqualTo("/")) + .withHeader("X-Amz-Target", containing("ListTables")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("x-amz-request-id", REQUEST_ID) + .withBody(LIST_TABLES))); + + // Stub out fake response for List S3 buckets + stubFor(put(urlEqualTo("/" + S3_BUCKET + "/" + S3_KEY)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("x-amz-request-id", REQUEST_ID) + .withBody(""))); // putObject returns no body + } + + @After + public void cleanup() { + EventBus.removeAllListeners(); + TransactionContext.clear(); + } + + @Rule + public WireMockRule wireMockRule = new WireMockRule(PORT); + + @Test + public void testNormalRequestInterception() { + DynamoDbClientBuilder builder = DynamoDbClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("x", "x"))) + .region(Region.US_WEST_2); + DynamoDbClient client = (DynamoDbClient) getTestableClient(builder).build(); + + client.listTables(); + + // Verify HTTP request was actually made + verify(postRequestedFor(urlEqualTo("/")).withHeader("X-Amz-Target", containing("ListTables"))); + + Assert.assertNotNull(testListener.requestEvent); + verifyEvent(testListener.requestEvent, "DynamoDb", "ListTables"); + Assert.assertEquals(Region.US_WEST_2.toString(), testListener.requestEvent.getRegion()); + Assert.assertTrue(testListener.requestEvent.getRequest() instanceof SdkRequest); + + Assert.assertNotNull(testListener.responseEvent); + verifyEvent(testListener.responseEvent, "DynamoDb", "ListTables"); + Assert.assertEquals(200, testListener.responseEvent.getStatusCode()); + Assert.assertEquals(REQUEST_ID, testListener.responseEvent.getRequestId()); + Assert.assertEquals(0, testListener.responseEvent.getRetryCount()); + Assert.assertTrue(testListener.responseEvent.getResponse() instanceof SdkResponse); + Assert.assertNull(testListener.responseEvent.getThrown()); + } + + @Test + public void testS3RequestInterception() throws URISyntaxException { + S3ClientBuilder builder = S3Client.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("x", "x"))) + .region(Region.US_WEST_2); + S3Client s3Client = (S3Client) getTestableClient(builder).build(); + + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket("myBucket") + .key("myKey") + .build(); + + Path filePath = Paths.get(AWSV2InterceptionTests.class.getResource("/software/amazon/disco/agent/integtest/s3File.txt").toURI()); + + s3Client.putObject(putObjectRequest, filePath); + + // Verify HTTP request was actually made + verify(putRequestedFor(urlEqualTo("/" + S3_BUCKET + "/" + S3_KEY))); + + Assert.assertNotNull(testListener.requestEvent); + verifyEvent(testListener.requestEvent, "S3", "PutObject"); + Assert.assertEquals(Region.US_WEST_2.toString(), testListener.requestEvent.getRegion()); + Assert.assertTrue(testListener.requestEvent.getRequest() instanceof SdkRequest); + + Assert.assertNotNull(testListener.responseEvent); + verifyEvent(testListener.responseEvent, "S3", "PutObject"); + Assert.assertEquals(200, testListener.responseEvent.getStatusCode()); + Assert.assertEquals(REQUEST_ID, testListener.responseEvent.getRequestId()); + Assert.assertTrue(testListener.responseEvent.getResponse() instanceof SdkResponse); + Assert.assertNull(testListener.responseEvent.getThrown()); + Assert.assertEquals(0, testListener.responseEvent.getRetryCount()); + } + + @Test + public void testReplaceHeader() { + EventBus.addListener(new HeaderListener()); + DynamoDbClientBuilder builder = DynamoDbClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("x", "x"))) + .region(Region.US_WEST_2); + DynamoDbClient client = (DynamoDbClient) getTestableClient(builder).build(); + + client.listTables(); + + verify(postRequestedFor(urlEqualTo("/")) + .withHeader("X-Amz-Target", containing("ListTables")) + .withHeader(HEADER_KEY, equalTo(HEADER_VAL))); + } + + @Test + public void testRetriesRecorded() { + // Set up a scenario where the service fails once then succeeds + String RETURN_SUCCESS_STATE = "return success"; + stubFor(post(urlEqualTo("/")).inScenario("retries") + .whenScenarioStateIs(STARTED) + .withHeader("X-Amz-Target", containing("DescribeTable")) + .willReturn(aResponse() + .withStatus(500) + .withBody("A service error occurred")) + .willSetStateTo(RETURN_SUCCESS_STATE)); + + stubFor(post(urlEqualTo("/")).inScenario("retries") + .whenScenarioStateIs(RETURN_SUCCESS_STATE) + .withHeader("X-Amz-Target", containing("DescribeTable")) + .willReturn(aResponse() + .withStatus(200) + .withBody("{}"))); + + DynamoDbClientBuilder builder = DynamoDbClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("x", "x"))) + .region(Region.US_WEST_2); + DynamoDbClient client = (DynamoDbClient) getTestableClient(builder).build(); + + DescribeTableRequest describeTableRequest = DescribeTableRequest.builder().tableName("myTable").build(); + client.describeTable(describeTableRequest); + + Assert.assertNotNull(testListener.responseEvent); + Assert.assertTrue(testListener.responseEvent.getResponse() instanceof SdkResponse); + Assert.assertEquals(1, testListener.responseEvent.getRetryCount()); // Exactly one retry for one failure + } + + @Test + public void testAwsServiceFailure() { + stubFor(post(urlEqualTo("/")) + .withHeader("X-Amz-Target", containing("ListTables")) + .willReturn(aResponse() + .withStatus(500) + .withBody("A service error occurred"))); + + DynamoDbClientBuilder builder = DynamoDbClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("x", "x"))) + .region(Region.US_WEST_2); + DynamoDbClient client = (DynamoDbClient) getTestableClient(builder).build(); + DynamoDbException exception = null; + + try { + client.listTables(); + Assert.fail(); // Should throw exception from service error + } catch (DynamoDbException e) { + exception = e; + } + + Assert.assertNotNull(testListener.requestEvent); + verifyEvent(testListener.requestEvent, "DynamoDb", "ListTables"); + Assert.assertEquals(Region.US_WEST_2.toString(), testListener.requestEvent.getRegion()); + Assert.assertTrue(testListener.requestEvent.getRequest() instanceof SdkRequest); + + Assert.assertNotNull(testListener.responseEvent); + verifyEvent(testListener.responseEvent, "DynamoDb", "ListTables"); + Assert.assertNotNull(testListener.responseEvent.getThrown()); + Assert.assertEquals(exception, testListener.responseEvent.getThrown()); + Assert.assertNull(testListener.responseEvent.getResponse()); + + // None should be set because no attempt was made + Assert.assertEquals(500, testListener.responseEvent.getStatusCode()); + Assert.assertNull(testListener.responseEvent.getRequestId()); + } + + @Test + public void testAwsClientException() { + DynamoDbClientBuilder builder = DynamoDbClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("x", "x"))) + .region(Region.US_WEST_2); + DynamoDbClient client = (DynamoDbClient) getTestableClient(builder) + .endpointOverride(URI.create("http://some.fake.endpoint.moc/path/to/page")) + .build(); + SdkClientException exception = null; + + try { + client.listTables(); + Assert.fail(); // Request should throw + } catch (SdkClientException e) { + exception = e; + } + + Assert.assertNotNull(testListener.requestEvent); + verifyEvent(testListener.requestEvent, "DynamoDb", "ListTables"); + Assert.assertEquals(Region.US_WEST_2.toString(), testListener.requestEvent.getRegion()); + Assert.assertTrue(testListener.requestEvent.getRequest() instanceof SdkRequest); + + Assert.assertNotNull(testListener.responseEvent); + verifyEvent(testListener.responseEvent, "DynamoDb", "ListTables"); + Assert.assertNotNull(testListener.responseEvent.getThrown()); + Assert.assertEquals(exception, testListener.responseEvent.getThrown()); + Assert.assertNull(testListener.responseEvent.getResponse()); + + // None should be set because no attempt was made + Assert.assertEquals(-1, testListener.responseEvent.getStatusCode()); + Assert.assertNull(testListener.responseEvent.getRequestId()); + } + + private SdkClientBuilder getTestableClient(SdkClientBuilder builder) { + ClientOverrideConfiguration configuration = ClientOverrideConfiguration.builder() + .apiCallTimeout(Duration.ofSeconds(1)) + .build(); + + return builder + .overrideConfiguration(configuration) + .endpointOverride(URI.create(ENDPOINT)); + } + + private void verifyEvent(AwsServiceDownstreamEvent event, String service, String operation) { + Assert.assertEquals("AWSv2", event.getOrigin()); + Assert.assertEquals(service, event.getService()); + Assert.assertEquals(operation, event.getOperation()); + } + + private static class TestListener implements Listener { + AwsServiceDownstreamRequestEvent requestEvent; + AwsServiceDownstreamResponseEvent responseEvent; + + @Override + public int getPriority() { + return 0; + } + + @Override + public void listen(Event e) { + if (e instanceof AwsServiceDownstreamRequestEvent) { + requestEvent = (AwsServiceDownstreamRequestEvent) e; + } else if (e instanceof AwsServiceDownstreamResponseEvent) { + responseEvent = (AwsServiceDownstreamResponseEvent) e; + } else { + Assert.fail("Unexpected event"); + } + } + } + + private static class HeaderListener implements Listener { + + @Override + public int getPriority() { + return 1; + } + + @Override + public void listen(Event e) { + if (e instanceof HeaderReplaceable) { + ((HeaderReplaceable) e).replaceHeader(HEADER_KEY, HEADER_VAL); + } + } + } +} diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/resources/software/amazon/disco/agent/integtest/s3File.txt b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/resources/software/amazon/disco/agent/integtest/s3File.txt new file mode 100644 index 0000000..2814b81 --- /dev/null +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/src/test/resources/software/amazon/disco/agent/integtest/s3File.txt @@ -0,0 +1 @@ +Dummy file to "upload" to S3 \ No newline at end of file diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptor.java b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptor.java index 6bb1079..5e31bc4 100644 --- a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptor.java +++ b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/awsv2/DiscoExecutionInterceptor.java @@ -113,14 +113,16 @@ public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes @Override public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { AwsServiceDownstreamRequestEventImpl requestEvent = (AwsServiceDownstreamRequestEventImpl) TransactionContext.getMetadata(TX_REQUEST_EVENT_KEY); + SdkHttpRequest sdkHttpRequest = context.httpRequest(); - requestEvent.withSdkHttpRequest(context.httpRequest()) + requestEvent.withSdkHttpRequest(sdkHttpRequest) .withHeaderMap(context.httpRequest().headers()); - // Modification may happen by consumers of the request event + // Modification may happen by consumers of the request event, namely adding headers EventBus.publish(requestEvent); - return context.httpRequest(); + // Return the (potentially) modified sdkHttpRequest, or the original sdkHttpRequest if modification failed + return requestEvent.getSdkHttpRequest() != null ? requestEvent.getSdkHttpRequest() : context.httpRequest(); } /** @@ -153,6 +155,7 @@ public void afterExecution(Context.AfterExecution context, ExecutionAttributes e AwsServiceDownstreamRequestEvent requestEvent = (AwsServiceDownstreamRequestEvent) TransactionContext.getMetadata(TX_REQUEST_EVENT_KEY); Object txRetryCount = TransactionContext.getMetadata(TX_RETRY_COUNT_KEY); + TransactionContext.removeMetadata(TX_RETRY_COUNT_KEY); // Prevent retry count from leaking across requests int retryCount = txRetryCount == null ? 0 : (int) txRetryCount; SdkHttpResponse httpResponse = context.httpResponse(); @@ -179,6 +182,7 @@ public void onExecutionFailure(Context.FailedExecution context, ExecutionAttribu AwsServiceDownstreamRequestEvent requestEvent = (AwsServiceDownstreamRequestEvent) TransactionContext.getMetadata(TX_REQUEST_EVENT_KEY); Object txRetryCount = TransactionContext.getMetadata(TX_RETRY_COUNT_KEY); + TransactionContext.removeMetadata(TX_RETRY_COUNT_KEY); // Prevent retry count from leaking across requests int retryCount = txRetryCount == null ? 0 : (int) txRetryCount; AwsServiceDownstreamResponseEvent awsEvent = new AwsServiceDownstreamResponseEventImpl(requestEvent) diff --git a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEventImpl.java b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEventImpl.java index 6a38482..8d2933b 100644 --- a/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEventImpl.java +++ b/disco-java-agent-aws/src/main/java/software/amazon/disco/agent/event/AwsServiceDownstreamRequestEventImpl.java @@ -39,6 +39,13 @@ public AwsServiceDownstreamRequestEventImpl(String origin, String service, Strin super(origin, service, operation); } + /** + * @return the underlying SdkHttpRequest + */ + public SdkHttpRequest getSdkHttpRequest() { + return sdkHttpRequest; + } + /** * Set the region for this event * @param region the region @@ -92,8 +99,7 @@ public boolean replaceHeader(String name, String value) { .appendHeader(name, value) .build(); - Map> headerMap = this.sdkHttpRequest.headers(); - this.withHeaderMap(headerMap); - return headerMap != null; + this.withHeaderMap(this.sdkHttpRequest.headers()); + return this.getHeaderMap() != null; } } From c89ee7bbd9ef8776a16b39eb93ccdd98da0a0ed6 Mon Sep 17 00:00:00 2001 From: William Armiros Date: Tue, 25 Aug 2020 12:14:00 -0700 Subject: [PATCH 45/45] Refactored build logic to enable publishing to maven central, and added BOM module --- CHANGELOG.md | 26 ++ README.md | 45 ++-- build.gradle.kts | 231 ++++++++++-------- disco-java-agent-aws/CODE_OF_CONDUCT.md | 4 - disco-java-agent-aws/CONTRIBUTING.md | 61 ----- disco-java-agent-aws/build.gradle.kts | 13 +- .../disco-java-agent-aws-api/README.md | 26 ++ .../disco-java-agent-aws-api/build.gradle.kts | 12 +- .../gradle.properties | 16 -- .../disco-java-agent-aws-plugin/README.md | 4 + .../build.gradle.kts | 18 +- .../gradle.properties | 16 -- .../CODE_OF_CONDUCT.md | 4 - .../CONTRIBUTING.md | 61 ----- .../build.gradle.kts | 4 + .../CODE_OF_CONDUCT.md | 4 - disco-java-agent-example-test/CONTRIBUTING.md | 61 ----- .../build.gradle.kts | 4 + disco-java-agent-example/CODE_OF_CONDUCT.md | 4 - disco-java-agent-example/CONTRIBUTING.md | 61 ----- disco-java-agent-example/build.gradle.kts | 1 + .../build.gradle.kts | 3 +- disco-java-agent-sql/CODE_OF_CONDUCT.md | 4 - disco-java-agent-sql/CONTRIBUTING.md | 61 ----- disco-java-agent-sql/build.gradle.kts | 13 +- .../build.gradle.kts | 10 +- .../gradle.properties | 16 -- disco-java-agent-sql/gradle.properties | 16 -- disco-java-agent-web/CODE_OF_CONDUCT.md | 4 - disco-java-agent-web/CONTRIBUTING.md | 61 ----- disco-java-agent-web/build.gradle.kts | 13 +- .../build.gradle.kts | 10 +- .../gradle.properties | 16 -- disco-java-agent-web/gradle.properties | 16 -- .../disco-java-agent-api/CODE_OF_CONDUCT.md | 4 - .../disco-java-agent-api/CONTRIBUTING.md | 61 ----- .../disco-java-agent-api/build.gradle.kts | 9 +- .../disco-java-agent-api/gradle.properties | 16 -- .../disco-java-agent-core/CODE_OF_CONDUCT.md | 4 - .../disco-java-agent-core/CONTRIBUTING.md | 61 ----- .../disco-java-agent-core/build.gradle.kts | 13 +- .../disco-java-agent-core/gradle.properties | 16 -- .../CODE_OF_CONDUCT.md | 4 - .../CONTRIBUTING.md | 61 ----- .../build.gradle.kts | 10 +- .../gradle.properties | 16 -- .../CODE_OF_CONDUCT.md | 4 - .../CONTRIBUTING.md | 61 ----- .../build.gradle.kts | 13 +- .../gradle.properties | 16 -- .../disco-java-agent/CODE_OF_CONDUCT.md | 4 - .../disco-java-agent/CONTRIBUTING.md | 61 ----- .../disco-java-agent/build.gradle.kts | 10 +- .../disco-java-agent/gradle.properties | 16 -- .../build.gradle.kts | 23 +- gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 35 ++- gradlew.bat | 25 +- settings.gradle.kts | 2 + 60 files changed, 309 insertions(+), 1157 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 disco-java-agent-aws/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent-aws/CONTRIBUTING.md delete mode 100644 disco-java-agent-aws/disco-java-agent-aws-api/gradle.properties delete mode 100644 disco-java-agent-aws/disco-java-agent-aws-plugin/gradle.properties delete mode 100644 disco-java-agent-example-injector-test/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent-example-injector-test/CONTRIBUTING.md delete mode 100644 disco-java-agent-example-test/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent-example-test/CONTRIBUTING.md delete mode 100644 disco-java-agent-example/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent-example/CONTRIBUTING.md delete mode 100644 disco-java-agent-sql/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent-sql/CONTRIBUTING.md delete mode 100644 disco-java-agent-sql/disco-java-agent-sql-plugin/gradle.properties delete mode 100644 disco-java-agent-sql/gradle.properties delete mode 100644 disco-java-agent-web/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent-web/CONTRIBUTING.md delete mode 100644 disco-java-agent-web/disco-java-agent-web-plugin/gradle.properties delete mode 100644 disco-java-agent-web/gradle.properties delete mode 100644 disco-java-agent/disco-java-agent-api/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent/disco-java-agent-api/CONTRIBUTING.md delete mode 100644 disco-java-agent/disco-java-agent-api/gradle.properties delete mode 100644 disco-java-agent/disco-java-agent-core/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent/disco-java-agent-core/CONTRIBUTING.md delete mode 100644 disco-java-agent/disco-java-agent-core/gradle.properties delete mode 100644 disco-java-agent/disco-java-agent-inject-api/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent/disco-java-agent-inject-api/CONTRIBUTING.md delete mode 100644 disco-java-agent/disco-java-agent-inject-api/gradle.properties delete mode 100644 disco-java-agent/disco-java-agent-plugin-api/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent/disco-java-agent-plugin-api/CONTRIBUTING.md delete mode 100644 disco-java-agent/disco-java-agent-plugin-api/gradle.properties delete mode 100644 disco-java-agent/disco-java-agent/CODE_OF_CONDUCT.md delete mode 100644 disco-java-agent/disco-java-agent/CONTRIBUTING.md delete mode 100644 disco-java-agent/disco-java-agent/gradle.properties rename {disco-java-agent => disco-toolkit-bom}/build.gradle.kts (56%) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6f3f6ec --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Change Log + +## Version 0.10.0 - 08/25/2020 + +* Added SQL interception package [PR #10](https://github.com/awslabs/disco/pull/10) +* Added AWS interception package [PR #10](https://github.com/awslabs/disco/pull/10) +* Added instrumentation preprocess package [PR #10](https://github.com/awslabs/disco/pull/10) +* Added Bill of Materials [PR #10](https://github.com/awslabs/disco/pull/10) +* Added installable `Package` class for collections of installables [PR #10](https://github.com/awslabs/disco/pull/10) +* Added concurrency support for `ScheduledThreadPoolExecutor` [PR #10](https://github.com/awslabs/disco/pull/10) +* Added Service downstream cancellation events [PR #10](https://github.com/awslabs/disco/pull/10) +* Added `HeaderReplaceable` interface for event classes [PR #10](https://github.com/awslabs/disco/pull/10) +* Added `removeMetadata` method for Transaction Context [PR #10](https://github.com/awslabs/disco/pull/10) +* Added support in core package for preprocess build tool [PR #10](https://github.com/awslabs/disco/pull/10) +* Fixed deprecated reflective access in `ForkJoinTask` tests [PR #10](https://github.com/awslabs/disco/pull/10) +* Fixed null pointer issues in `HttpResponseEvent` and `TransactionContext` [PR #10](https://github.com/awslabs/disco/pull/10) +* Fixed `ExecutorService` to use re-entrancy check [PR #10](https://github.com/awslabs/disco/pull/10) +* Fixed flaky TX tests [PR #10](https://github.com/awslabs/disco/pull/10) +* Ensure transaction context is propagated for nested executor submissions [PR #10](https://github.com/awslabs/disco/pull/10) +* Deprecated `MethodHandleWrapper` class [PR #10](https://github.com/awslabs/disco/pull/10) +* Upgraded ByteBuddy to 1.10.14 and ASM to 8.0.1 [PR #10](https://github.com/awslabs/disco/pull/10) +* Upgraded to Gradle 6.6 [PR #10](https://github.com/awslabs/disco/pull/10) + +## Version 0.9.1 - 12/2/2019 + +* Initial commit of DiSCo Toolkit \ No newline at end of file diff --git a/README.md b/README.md index 14cd1a2..e96e5ec 100644 --- a/README.md +++ b/README.md @@ -170,36 +170,43 @@ Unfortunately this can mean that they *sometimes* fail and require restarting. W ### Including Disco as a dependency in your product -Until we publish to Maven, you can run ``./gradlew publishToMavenLocal``, and consume from your local Maven cache -in your Maven or Gradle builds with e.g: - -
+Disco is available in Maven Central. A Bill of Materials (BOM) package is vended to make depending on multiple +Disco packages easier. #### Using Maven coordinates ```xml - - software.amazon.disco - disco-java-agent-api - 0.9.2 - + + + + software.amazon.disco + disco-toolkit-bom + 0.10.0 + pom + import + + + + + + software.amazon.disco + disco-java-agent-api + + + ``` #### Using Gradle's default DSL ```groovy -repositories { - mavenLocal() -} - -compile 'software.amazon.disco:disco-java-agent-api:0.9.2' +implementation platform('software.amazon.disco:disco-toolkit-bom:0.10.0') +implementation 'software.amazon.disco:disco-java-agent-api' +// Other disco dependencies ``` #### Using Gradle's Kotlin DSL ```kotlin -repositories { - mavenLocal() -} - -compile("software.amazon.disco", "disco-java-agent-api", "0.9.2") +implementation(platform("software.amazon.disco:disco-toolkit-bom:0.10.0")) +implementation("software.amazon.disco:disco-java-agent-api") +// Other disco dependencies ``` ### Troubleshooting diff --git a/build.gradle.kts b/build.gradle.kts index 484d8c6..41a257e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,162 +19,147 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar //TODO specify the versions of ByteBuddy and ASM in here, since they are used in a few places. plugins { id("com.github.johnrengelman.shadow") version "5.2.0" apply false - `java-library` - `maven-publish` -} - -//This is not a subproject that contains code, nor produces any artifacts. Disable the jar task -//to prevent a useless empty jar file being produced as a build side effect. -tasks { - named("jar") { - setEnabled(false) - } } subprojects { - apply() - - version = "0.9.2" + version = "0.10.0" repositories { mavenCentral() } - dependencies { - testImplementation("junit", "junit", "4.12") - testImplementation("org.mockito", "mockito-core", "3.+") - } - - configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - } - - tasks { - pluginManager.withPlugin("com.github.johnrengelman.shadow") { + // Set up creation of shaded Jars + pluginManager.withPlugin("com.github.johnrengelman.shadow") { + tasks { named("shadowJar") { //suppress the "-all" suffix on the jar name, simply replace the default built jar instead (disco-java-agent-web-plugin-x.y.z.jar) archiveClassifier.set(null as String?) - + //Must relocate both of these inner dependencies of the Disco agent, to avoid conflicts in your customer's application relocate("org.objectweb.asm", "software.amazon.disco.agent.jar.asm") relocate("net.bytebuddy", "software.amazon.disco.agent.jar.bytebuddy") } //once gradle has made its default jar, follow up by producing the shadow/uber jar - assemble { + named("assemble") { dependsOn(named("shadowJar")) } named("shadowJar") { - dependsOn(jar) + dependsOn(named("jar")) } } } - // This block only applies to plugin modules, as determined by the existence of a "-plugin" suffix - if (project.name.endsWith("-plugin")) { - // Remove "-plugin" suffix to get corresponding library name - val libraryName = ":" + project.name.subSequence(0, project.name.length - 7) - val ver = project.version - - // Configure dependencies common to plugins + plugins.withId("java-library") { dependencies { - runtimeOnly(project(libraryName)) { - // By setting the isTransitive flag false, we take only what is described by the above project, and not - // its entire closure of transitive dependencies (i.e. all of Core, all of Bytebuddy, etc) - // this makes our generated Jar minimal, containing only our source files, and our manifest. All those - // other dependencies are expected to be in the base agent, which loads this plugin. - isTransitive = false - } + add("testImplementation", "junit:junit:4.12") + add("testImplementation", "org.mockito:mockito-core:3.+") + } - // Test target is integ tests for Disco plugins. Some classes in the integ tests also self-test via - // little unit tests during testrun. - testImplementation(project(":disco-java-agent:disco-java-agent-api")) - testImplementation("org.mockito", "mockito-core", "1.+") + configure { + sourceCompatibility = JavaVersion.VERSION_1_8 } - // Configure integ tests, which need a loaded agent, and the loaded plugin - tasks.test { - // explicitly remove the runtime classpath from the tests since they are integ tests, and may not access the - // dependency we acquired in order to build the plugin, namely the library jar for this plugin which makes reference - // to byte buddy classes which have NOT been relocated by a shadowJar rule. Discovering those unrelocated classes - // would not be possible in a real client installation, and would cause plugin loading to fail. - classpath = classpath.minus(configurations.runtimeClasspath.get()) - - //load the agent for the tests, and have it discover the plugin - jvmArgs("-javaagent:../../disco-java-agent/disco-java-agent/build/libs/disco-java-agent-$ver.jar=pluginPath=./build/libs:extraverbose") - - //we do not take any normal compile/runtime dependency on this, but it must be built first since the above jvmArg - //refers to its built artifact. - dependsOn(":disco-java-agent:disco-java-agent:build") - dependsOn("$libraryName:${project.name}:assemble") + // This block only applies to plugin modules, as determined by the existence of a "-plugin" suffix + if (project.name.endsWith("-plugin")) { + // Remove "-plugin" suffix to get corresponding library name + val libraryName = ":" + project.name.subSequence(0, project.name.length - 7) + val ver = project.version + + // Configure dependencies common to plugins + dependencies { + add("runtimeOnly", project(libraryName)) { + // By setting the isTransitive flag false, we take only what is described by the above project, and not + // its entire closure of transitive dependencies (i.e. all of Core, all of Bytebuddy, etc) + // this makes our generated Jar minimal, containing only our source files, and our manifest. All those + // other dependencies are expected to be in the base agent, which loads this plugin. + isTransitive = false + } + + // Test target is integ tests for Disco plugins. Some classes in the integ tests also self-test via + // little unit tests during testrun. + add("testImplementation", project(":disco-java-agent:disco-java-agent-api")) + add("testImplementation", "org.mockito:mockito-core:1.+") + } + + // Configure integ tests, which need a loaded agent, and the loaded plugin + tasks.named("test") { + // explicitly remove the runtime classpath from the tests since they are integ tests, and may not access the + // dependency we acquired in order to build the plugin, namely the library jar for this plugin which makes reference + // to byte buddy classes which have NOT been relocated by a shadowJar rule. Discovering those unrelocated classes + // would not be possible in a real client installation, and would cause plugin loading to fail. + classpath = classpath.minus(configurations.named("runtimeClasspath").get()) + + //load the agent for the tests, and have it discover the plugin + jvmArgs("-javaagent:../../disco-java-agent/disco-java-agent/build/libs/disco-java-agent-$ver.jar=pluginPath=./build/libs:extraverbose") + + //we do not take any normal compile/runtime dependency on this, but it must be built first since the above jvmArg + //refers to its built artifact. + dependsOn(":disco-java-agent:disco-java-agent:build") + dependsOn("$libraryName:${project.name}:assemble") + } } } //we publish everything except example subprojects to maven. Projects which desire to be published to maven express the intent - //via a property called simply 'maven' in their gradle.properties file (if it exists at all). - //Each package to be published still needs a small amount of boilerplate to express whether is is a 'normal' - //library which expresses its dependencies, or a shadowed library which includes and hides them. For a normal one - //e.g. Core or Web, that boilerplate may look like: - // configure { - // publications { - // named("maven") { - // from(components["java"]) - // } - // } - // } - // - // whereas a shadowed artifact would be declared along the lines of: - // - // configure { - // publications { - // named("maven") { - // artifact(tasks.jar.get()) - // } - // } - // } - // - //which declares the jar and the jar alone as the artifact, suppressing the default behaviour of gathering dependency info - // - //TODO: find a way to express this just once in the block below, probably by inspecting the existence or absence - //of the shadow plugin, or the ShadowJar task. So far attempts to consolidate this logic have not succeeded, hence - //the current need for the above boilerplate. + //by using the "maven-publish" plugin. // //TODO: apply some continuous integration rules to publish to Maven automatically when e.g. version number increases // //Publication to local Maven is simply "./gradlew publishToMavenLocal" - if (hasProperty("maven")) { - apply(plugin = "maven-publish") + plugins.withId("maven-publish") { + plugins.apply("signing") + + plugins.withId("java-library") { + //create a task to publish our sources + tasks.register("sourcesJar") { + from(project.the()["main"].allJava) + archiveClassifier.set("sources") + } - //create a task to publish our sources - tasks.register("sourcesJar") { - from(sourceSets.main.get().allJava) - archiveClassifier.set("sources") + //create a task to publish javadoc + tasks.register("javadocJar") { + from(tasks.named("javadoc")) + archiveClassifier.set("javadoc") + } } - //create a task to publish javadoc - tasks.register("javadocJar") { - from(tasks.javadoc) - archiveClassifier.set("javadoc") + // Disable publishing a bunch of unnecessary Gradle metadata files + tasks.withType { + enabled = false } //defer maven publish until the assemble task has finished, giving time for shadowJar to complete, if it is present tasks.withType { - dependsOn(tasks.assemble) + dependsOn(tasks.named("assemble")) } //all our maven publications have similar characteristics, declare as much as possible here at the top level configure { - repositories { - maven { - } - } publications { create("maven") { - artifact(tasks["sourcesJar"]) - artifact(tasks["javadocJar"]) - groupId = "software.amazon.disco" + // Define what artifact to publish depending on what plugin is present + // If shadow is present, we should publish the shaded JAR + // Otherwise, publish the standard JAR from compilation + plugins.withId("java-library") { + if (plugins.hasPlugin("com.github.johnrengelman.shadow")) { + artifact(tasks.named("jar").get()) + } else { + from(components["java"]) + } + + artifact(tasks["sourcesJar"]) + artifact(tasks["javadocJar"]) + } + + // For publishing the BOM + plugins.withId("java-platform") { + from(components["javaPlatform"]) + } + + groupId = rootProject.name pom { name.set(groupId + ":" + artifactId) @@ -192,6 +177,26 @@ subprojects { name.set("Paul Connell") email.set("connellp@amazon.com") } + developer { + id.set("armiros") + name.set("William Armiros") + email.set("armiros@amazon.com") + } + developer { + id.set("ssirip") + name.set("Sai Siripurapu") + email.set("ssirip@amazon.com") + } + developer { + id.set("liuhongb") + name.set("Hongbo Liu") + email.set("liuhongb@amazon.com") + } + developer { + id.set("besmithe") + name.set("Ben Smithers") + email.set("besmithe@amazon.co.uk") + } } scm { connection.set("scm:git:git://github.com/awslabs/disco.git") @@ -201,7 +206,21 @@ subprojects { } } } + + repositories { + maven { + url = uri("https://aws.oss.sonatype.org/service/local/staging/deploy/maven2/") + credentials { + username = "${findProperty("disco.sonatype.username")}" + password = "${findProperty("disco.sonatype.password")}" + } + } + } + } + + configure { + useGpgCmd() + sign(the().publications["maven"]) } } } - diff --git a/disco-java-agent-aws/CODE_OF_CONDUCT.md b/disco-java-agent-aws/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent-aws/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent-aws/CONTRIBUTING.md b/disco-java-agent-aws/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent-aws/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent-aws/build.gradle.kts b/disco-java-agent-aws/build.gradle.kts index d0a6771..4f48294 100644 --- a/disco-java-agent-aws/build.gradle.kts +++ b/disco-java-agent-aws/build.gradle.kts @@ -13,6 +13,11 @@ * permissions and limitations under the License. */ +plugins { + `java-library` + `maven-publish` +} + dependencies { // Compile against AWS SDK V2, but we do not take a runtime dependency on it compileOnly("software.amazon.awssdk", "sdk-core", "2.13.76") @@ -25,11 +30,3 @@ dependencies { testImplementation("com.amazonaws", "aws-java-sdk-sqs", "1.11.840") testImplementation("software.amazon.awssdk", "dynamodb", "2.13.76") } - -configure { - publications { - named("maven") { - from(components["java"]) - } - } -} \ No newline at end of file diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/README.md b/disco-java-agent-aws/disco-java-agent-aws-api/README.md index c1224b2..01cc31f 100644 --- a/disco-java-agent-aws/disco-java-agent-aws-api/README.md +++ b/disco-java-agent-aws/disco-java-agent-aws-api/README.md @@ -3,3 +3,29 @@ This package contains classes and interfaces which are extended and implemented by the `disco-java-agent-aws` library as Disco Events. Consumers of Disco AWS Support can optionally depend on this package to cast Events received from the event bus to these types and not lose any AWS-SDK-specific data from downcasting. + +This package is included in the distributed `disco-java-agent-aws-plugin` JAR. If you are a consumer of the Disco Events +defined in this package produced from the AWS plugin, you can declare a `compileOnly` or `provided` dependency on this +package, as shown in these examples (assuming you've already declared a version with a dependency on the +`disco-toolkit-bom`). + +In Gradle's default DSL: + +```groovy +compileOnly 'software.amazon.disco:disco-java-agent-aws-api' +``` + +In Gradle's Kotlin DSL: + +```kotlin +compileOnly("software.amazon.disco:disco-java-agent-aws-api") +``` + +Using Maven: + +```xml + + software.amazon.disco + disco-java-agent-aws-api + +``` diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/build.gradle.kts b/disco-java-agent-aws/disco-java-agent-aws-api/build.gradle.kts index cf7319f..a24d092 100644 --- a/disco-java-agent-aws/disco-java-agent-aws-api/build.gradle.kts +++ b/disco-java-agent-aws/disco-java-agent-aws-api/build.gradle.kts @@ -12,15 +12,11 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ +plugins { + `java-library` + `maven-publish` +} dependencies { implementation(project(":disco-java-agent:disco-java-agent-api")) } - -configure { - publications { - named("maven") { - artifact(tasks.jar.get()) - } - } -} diff --git a/disco-java-agent-aws/disco-java-agent-aws-api/gradle.properties b/disco-java-agent-aws/disco-java-agent-aws-api/gradle.properties deleted file mode 100644 index d861bc6..0000000 --- a/disco-java-agent-aws/disco-java-agent-aws-api/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/README.md b/disco-java-agent-aws/disco-java-agent-aws-plugin/README.md index a64e848..e652f05 100644 --- a/disco-java-agent-aws/disco-java-agent-aws-plugin/README.md +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/README.md @@ -12,6 +12,10 @@ The build.gradle.kts file contains a build rule to generate an appropriate MANIF Inherited from a top level build.gradle.kts file in the top level project, the ByteBuddy and ASM dependencies are repackaged in agreement with the expectations of the disco-java-agent. +This plugin includes the `disco-java-agent-aws-api` package in its artifact so that it can be fully standalone. +If you would like to use the AWS API package and this one, ensure you declare a `compileOnly` dependency on it to +avoid the API classes appearing twice on the runtime classpath. + ### Integ Tests The test target in build.gradle.kts is configured to apply the disco-java-agent via an argument given to the diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts b/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts index 77d7c9f..b374932 100644 --- a/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts +++ b/disco-java-agent-aws/disco-java-agent-aws-plugin/build.gradle.kts @@ -14,6 +14,8 @@ */ plugins { + `java-library` + `maven-publish` id("com.github.johnrengelman.shadow") } @@ -37,13 +39,19 @@ val safetyTestImplementation by configurations.getting { extendsFrom(configurations.implementation.get()) } -// Only dependency the test needs is JUnit dependencies { + // Include the API sources in the plugin JAR, since they are referenced directly + runtimeOnly(project(":disco-java-agent-aws:disco-java-agent-aws-api")) { + isTransitive = false + } + testImplementation("com.amazonaws", "aws-java-sdk-dynamodb", "1.11.840") testImplementation("software.amazon.awssdk", "dynamodb", "2.13.76") testImplementation("software.amazon.awssdk", "s3", "2.13.76") testImplementation("com.github.tomakehurst", "wiremock-jre8", "2.27.0") testImplementation(project(":disco-java-agent-aws:disco-java-agent-aws-api")) + + // Only dependency the safety test needs is JUnit safetyTestImplementation("junit:junit:4.12") } @@ -68,11 +76,3 @@ val safetyTestTask = task("safetyTest") { tasks.check { dependsOn(safetyTestTask) } - -configure { - publications { - named("maven") { - artifact(tasks.jar.get()) - } - } -} diff --git a/disco-java-agent-aws/disco-java-agent-aws-plugin/gradle.properties b/disco-java-agent-aws/disco-java-agent-aws-plugin/gradle.properties deleted file mode 100644 index d861bc6..0000000 --- a/disco-java-agent-aws/disco-java-agent-aws-plugin/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent-example-injector-test/CODE_OF_CONDUCT.md b/disco-java-agent-example-injector-test/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent-example-injector-test/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent-example-injector-test/CONTRIBUTING.md b/disco-java-agent-example-injector-test/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent-example-injector-test/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent-example-injector-test/build.gradle.kts b/disco-java-agent-example-injector-test/build.gradle.kts index 7d23f41..b550f1a 100644 --- a/disco-java-agent-example-injector-test/build.gradle.kts +++ b/disco-java-agent-example-injector-test/build.gradle.kts @@ -13,6 +13,10 @@ * permissions and limitations under the License. */ +plugins { + `java-library` +} + dependencies { testImplementation(project(":disco-java-agent:disco-java-agent-api")) testImplementation(project(":disco-java-agent:disco-java-agent-inject-api", "shadow")) diff --git a/disco-java-agent-example-test/CODE_OF_CONDUCT.md b/disco-java-agent-example-test/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent-example-test/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent-example-test/CONTRIBUTING.md b/disco-java-agent-example-test/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent-example-test/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent-example-test/build.gradle.kts b/disco-java-agent-example-test/build.gradle.kts index e4f5a1c..10a708c 100644 --- a/disco-java-agent-example-test/build.gradle.kts +++ b/disco-java-agent-example-test/build.gradle.kts @@ -13,6 +13,10 @@ * permissions and limitations under the License. */ +plugins { + `java-library` +} + dependencies { testImplementation(project(":disco-java-agent:disco-java-agent-api")) testImplementation("javax.servlet", "javax.servlet-api", "3.0.1") diff --git a/disco-java-agent-example/CODE_OF_CONDUCT.md b/disco-java-agent-example/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent-example/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent-example/CONTRIBUTING.md b/disco-java-agent-example/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent-example/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent-example/build.gradle.kts b/disco-java-agent-example/build.gradle.kts index 497bf4b..37ff27a 100644 --- a/disco-java-agent-example/build.gradle.kts +++ b/disco-java-agent-example/build.gradle.kts @@ -14,6 +14,7 @@ */ plugins { + `java-library` id("com.github.johnrengelman.shadow") } diff --git a/disco-java-agent-instrumentation-preprocess/build.gradle.kts b/disco-java-agent-instrumentation-preprocess/build.gradle.kts index 7a8d0dc..e7634b1 100644 --- a/disco-java-agent-instrumentation-preprocess/build.gradle.kts +++ b/disco-java-agent-instrumentation-preprocess/build.gradle.kts @@ -14,6 +14,7 @@ */ plugins { + `java-library` id("io.freefair.lombok") version "5.1.0" id("com.github.johnrengelman.shadow") } @@ -76,4 +77,4 @@ val integtest = task("integtest") { tasks.build { dependsOn(integtest) -} \ No newline at end of file +} diff --git a/disco-java-agent-sql/CODE_OF_CONDUCT.md b/disco-java-agent-sql/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent-sql/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent-sql/CONTRIBUTING.md b/disco-java-agent-sql/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent-sql/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent-sql/build.gradle.kts b/disco-java-agent-sql/build.gradle.kts index caa2c5f..2b6da79 100644 --- a/disco-java-agent-sql/build.gradle.kts +++ b/disco-java-agent-sql/build.gradle.kts @@ -13,16 +13,13 @@ * permissions and limitations under the License. */ +plugins { + `java-library` + `maven-publish` +} + dependencies { implementation(project(":disco-java-agent:disco-java-agent-core")) testImplementation("org.mockito", "mockito-core", "1.+") testImplementation("mysql", "mysql-connector-java", "8.+") } - -configure { - publications { - named("maven") { - from(components["java"]) - } - } -} \ No newline at end of file diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts b/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts index 0ddc4be..9b97c3e 100644 --- a/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts +++ b/disco-java-agent-sql/disco-java-agent-sql-plugin/build.gradle.kts @@ -14,6 +14,8 @@ */ plugins { + `java-library` + `maven-publish` id("com.github.johnrengelman.shadow") } @@ -24,11 +26,3 @@ tasks.shadowJar { )) } } - -configure { - publications { - named("maven") { - artifact(tasks.jar.get()) - } - } -} diff --git a/disco-java-agent-sql/disco-java-agent-sql-plugin/gradle.properties b/disco-java-agent-sql/disco-java-agent-sql-plugin/gradle.properties deleted file mode 100644 index d861bc6..0000000 --- a/disco-java-agent-sql/disco-java-agent-sql-plugin/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent-sql/gradle.properties b/disco-java-agent-sql/gradle.properties deleted file mode 100644 index d861bc6..0000000 --- a/disco-java-agent-sql/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent-web/CODE_OF_CONDUCT.md b/disco-java-agent-web/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent-web/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent-web/CONTRIBUTING.md b/disco-java-agent-web/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent-web/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent-web/build.gradle.kts b/disco-java-agent-web/build.gradle.kts index b6b4485..46e8fad 100644 --- a/disco-java-agent-web/build.gradle.kts +++ b/disco-java-agent-web/build.gradle.kts @@ -13,17 +13,14 @@ * permissions and limitations under the License. */ +plugins { + `java-library` + `maven-publish` +} + dependencies { implementation(project(":disco-java-agent:disco-java-agent-core")) testImplementation("org.mockito", "mockito-core", "1.+") testImplementation("javax.servlet", "javax.servlet-api", "3.0.1") testImplementation("org.apache.httpcomponents", "httpclient", "4.5.10") } - -configure { - publications { - named("maven") { - from(components["java"]) - } - } -} \ No newline at end of file diff --git a/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts b/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts index d58c290..bcb51b7 100644 --- a/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts +++ b/disco-java-agent-web/disco-java-agent-web-plugin/build.gradle.kts @@ -14,6 +14,8 @@ */ plugins { + `java-library` + `maven-publish` id("com.github.johnrengelman.shadow") } @@ -29,11 +31,3 @@ tasks.shadowJar { )) } } - -configure { - publications { - named("maven") { - artifact(tasks.jar.get()) - } - } -} diff --git a/disco-java-agent-web/disco-java-agent-web-plugin/gradle.properties b/disco-java-agent-web/disco-java-agent-web-plugin/gradle.properties deleted file mode 100644 index 6bef8b6..0000000 --- a/disco-java-agent-web/disco-java-agent-web-plugin/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent-web/gradle.properties b/disco-java-agent-web/gradle.properties deleted file mode 100644 index 6bef8b6..0000000 --- a/disco-java-agent-web/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent/disco-java-agent-api/CODE_OF_CONDUCT.md b/disco-java-agent/disco-java-agent-api/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent/disco-java-agent-api/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent/disco-java-agent-api/CONTRIBUTING.md b/disco-java-agent/disco-java-agent-api/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent/disco-java-agent-api/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent/disco-java-agent-api/build.gradle.kts b/disco-java-agent/disco-java-agent-api/build.gradle.kts index ceba2ea..7137ec7 100644 --- a/disco-java-agent/disco-java-agent-api/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-api/build.gradle.kts @@ -13,10 +13,7 @@ * permissions and limitations under the License. */ -configure { - publications { - named("maven") { - from(components["java"]) - } - } +plugins { + `java-library` + `maven-publish` } diff --git a/disco-java-agent/disco-java-agent-api/gradle.properties b/disco-java-agent/disco-java-agent-api/gradle.properties deleted file mode 100644 index 6bef8b6..0000000 --- a/disco-java-agent/disco-java-agent-api/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent/disco-java-agent-core/CODE_OF_CONDUCT.md b/disco-java-agent/disco-java-agent-core/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent/disco-java-agent-core/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent/disco-java-agent-core/CONTRIBUTING.md b/disco-java-agent/disco-java-agent-core/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent/disco-java-agent-core/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent/disco-java-agent-core/build.gradle.kts b/disco-java-agent/disco-java-agent-core/build.gradle.kts index 04c394d..455a1c8 100644 --- a/disco-java-agent/disco-java-agent-core/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-core/build.gradle.kts @@ -13,20 +13,17 @@ * permissions and limitations under the License. */ +plugins { + `java-library` + `maven-publish` +} + dependencies { api(project(":disco-java-agent:disco-java-agent-plugin-api")) api(project(":disco-java-agent:disco-java-agent-inject-api")) api(project(":disco-java-agent:disco-java-agent-api")) } -configure { - publications { - named("maven") { - from(components["java"]) - } - } -} - /** * Define a secondary set of tests, for testing the actual interceptions provided by the Installables. */ diff --git a/disco-java-agent/disco-java-agent-core/gradle.properties b/disco-java-agent/disco-java-agent-core/gradle.properties deleted file mode 100644 index 6bef8b6..0000000 --- a/disco-java-agent/disco-java-agent-core/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent/disco-java-agent-inject-api/CODE_OF_CONDUCT.md b/disco-java-agent/disco-java-agent-inject-api/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent/disco-java-agent-inject-api/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent/disco-java-agent-inject-api/CONTRIBUTING.md b/disco-java-agent/disco-java-agent-inject-api/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent/disco-java-agent-inject-api/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts b/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts index df64f9a..debd729 100644 --- a/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-inject-api/build.gradle.kts @@ -14,6 +14,8 @@ */ plugins { + `java-library` + `maven-publish` id("com.github.johnrengelman.shadow") } @@ -23,11 +25,3 @@ dependencies { implementation("net.bytebuddy", "byte-buddy-agent", "1.10.14") testImplementation("net.bytebuddy", "byte-buddy-dep", "1.10.14") } - -configure { - publications { - named("maven") { - artifact(tasks.jar.get()) - } - } -} diff --git a/disco-java-agent/disco-java-agent-inject-api/gradle.properties b/disco-java-agent/disco-java-agent-inject-api/gradle.properties deleted file mode 100644 index 6bef8b6..0000000 --- a/disco-java-agent/disco-java-agent-inject-api/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent/disco-java-agent-plugin-api/CODE_OF_CONDUCT.md b/disco-java-agent/disco-java-agent-plugin-api/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent/disco-java-agent-plugin-api/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent/disco-java-agent-plugin-api/CONTRIBUTING.md b/disco-java-agent/disco-java-agent-plugin-api/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent/disco-java-agent-plugin-api/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts b/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts index fc00507..ba9d38e 100644 --- a/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts +++ b/disco-java-agent/disco-java-agent-plugin-api/build.gradle.kts @@ -13,6 +13,11 @@ * permissions and limitations under the License. */ +plugins { + `java-library` + `maven-publish` +} + dependencies { //TODO update BB and ASM to latest api("net.bytebuddy", "byte-buddy-dep", "1.10.14") @@ -20,11 +25,3 @@ dependencies { implementation("org.ow2.asm", "asm-commons", "8.0.1") implementation("org.ow2.asm", "asm-tree", "8.0.1") } - -configure { - publications { - named("maven") { - from(components["java"]) - } - } -} diff --git a/disco-java-agent/disco-java-agent-plugin-api/gradle.properties b/disco-java-agent/disco-java-agent-plugin-api/gradle.properties deleted file mode 100644 index 6bef8b6..0000000 --- a/disco-java-agent/disco-java-agent-plugin-api/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent/disco-java-agent/CODE_OF_CONDUCT.md b/disco-java-agent/disco-java-agent/CODE_OF_CONDUCT.md deleted file mode 100644 index 5b627cf..0000000 --- a/disco-java-agent/disco-java-agent/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,4 +0,0 @@ -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/disco-java-agent/disco-java-agent/CONTRIBUTING.md b/disco-java-agent/disco-java-agent/CONTRIBUTING.md deleted file mode 100644 index 5ad897b..0000000 --- a/disco-java-agent/disco-java-agent/CONTRIBUTING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Contributing Guidelines - -Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional -documentation, we greatly value feedback and contributions from our community. - -Please read through this document before submitting any issues or pull requests to ensure we have all the necessary -information to effectively respond to your bug report or contribution. - - -## Reporting Bugs/Feature Requests - -We welcome you to use the GitHub issue tracker to report bugs or suggest features. - -When filing an issue, please check [existing open](https://github.com/awslabs/disco/issues), or [recently closed](https://github.com/awslabs/disco/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - - -## Contributing via Pull Requests -Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: - -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. - -To send us a pull request, please: - -1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. -4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. -6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. - -GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and -[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - - -## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/disco/labels/help%20wanted) issues is a great place to start. - - -## Code of Conduct -This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). -For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact -opensource-codeofconduct@amazon.com with any additional questions or comments. - - -## Security issue notifications -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. - - -## Licensing - -See the [LICENSE](https://github.com/awslabs/disco/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - -We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/disco-java-agent/disco-java-agent/build.gradle.kts b/disco-java-agent/disco-java-agent/build.gradle.kts index e2dbedb..abc8710 100644 --- a/disco-java-agent/disco-java-agent/build.gradle.kts +++ b/disco-java-agent/disco-java-agent/build.gradle.kts @@ -14,6 +14,8 @@ */ plugins { + `java-library` + `maven-publish` id("com.github.johnrengelman.shadow") } @@ -35,11 +37,3 @@ tasks.shadowJar { )) } } - -configure { - publications { - named("maven") { - artifact(tasks.jar.get()) - } - } -} diff --git a/disco-java-agent/disco-java-agent/gradle.properties b/disco-java-agent/disco-java-agent/gradle.properties deleted file mode 100644 index 6bef8b6..0000000 --- a/disco-java-agent/disco-java-agent/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# or in the "license" file accompanying this file. This file is distributed -# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -# express or implied. See the License for the specific language governing -# permissions and limitations under the License. -# - -maven = true \ No newline at end of file diff --git a/disco-java-agent/build.gradle.kts b/disco-toolkit-bom/build.gradle.kts similarity index 56% rename from disco-java-agent/build.gradle.kts rename to disco-toolkit-bom/build.gradle.kts index 9a5f625..dc2cb6c 100644 --- a/disco-java-agent/build.gradle.kts +++ b/disco-toolkit-bom/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -13,10 +13,19 @@ * permissions and limitations under the License. */ -//This is not a subproject that contains code, nor produces any artifacts. Disable the jar task -//to prevent a useless empty jar file being produced as a build side effect. -tasks { - named("jar") { - setEnabled(false) +plugins { + `java-platform` + `maven-publish` +} + +dependencies { + constraints { + rootProject.subprojects { + plugins.withId("maven-publish") { + if (name.startsWith("disco-java-agent") && !name.endsWith("-bom")) { + api("${rootProject.name}:${name}:${version}") + } + } + } } -} \ No newline at end of file +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 23334 zcmZ6yQ*_^7)b$%Swr#tyZQHhuU-WHk+qUgAc4J!&nxrusy#I5a=UlvJjD59l*Pe6C zy*_IVG(!&0LN+phBc)L-m3M)If#E@dfw80{QedYjfnx%cY|Q2krta=>YST_jBA9|p zot|vvp%0RvR1srYTl+z-NNCL@5oSg;&!BaMOR}sfJn192cT55<(x!dL7ut~~3^-Ur z4>ora_t}-M=h->qJpjxnx)1EWvn8?z{O>`3f+7iuKL<2+zHP~ldyrmD0P{Z4X%%`W zo_)z~Yy==^IcLFQUXFGeH8WebVkw~L>r{vkbd$z5MQq(ni#a^*>hw=_Z;C^Gfrdev z!mgg_pG zeMQUU+?X~Em$z2qQyLw%`*oeVS_0m|fcm)7q6xUbNU;Eku2#8)2t3}hj!-y+-89iQ z3fZ2srkJN7rV0vd0?Or&O+;oeJrGw6+{`LpB@d3*VpO>Un|q3BNDJspjozc(4hJDz zwgOl$df!`k*;k(~&;GPfVBAD3Hi3C}ZFV~#*$f>4hj%YsCq6tRQfp_Dt-)S_Uj!o= ze~fwe`&6h3{1?2yCfi zXybknxod^Z|~hQkrhOl74q z$G@Js5lv&IFx8Sm%&;&R^ZS012w;u(#-d_d7z}E<_L7JxsnmzL7!JXpt9>W$Br_-E zrt)8pGV-SsMKD!epNc6VMP@dY9SZ~}4KEJ0{AM}D(Ur&6>Xwy(7hK_??ybcBfV^H zx_aQ9cAG-(o3ZK6^5ob$c;XQ+WUNPojJo*4bQPb@#nF;E%h&FNJuVpSRK{}ljl}!b z#w$tS(t%=z)Q_2_4&C(JNz3Z&rgJG<@$5eR{6=#eNx!WXg2rrliM1=mC{vw4N32Vt z(hz+({@Wh2Y$x_R-d{$2XdqlCZW<@Yvix3|nho{g3fcY`x3r&v zC3T%<=pJrdP1&am@lIKma2=I=^4+>BZP8iAC+!5rKrxkP-K0t^lPkRKzej86htd0P z#d#*bI0LJ?=)BWl*(f{h=~UK26R;3?r6Z!LAuS$vtfd9{cVHb61Hh{>!#phiJ%Th9 zF?=-pJ;B(60kgq8M!6s_=E5q^V1BZqUk45QP(0*!5vKTDdWw8Z2W(yF7Cd4q6#8Au zDKAwS7y&OlW39}KP7u;mRY_qmKm6ZlbFdopRZRb2WvuPtfGOrS@2QJ&4I=v~NILZ5 zeRhAPI(ofewJkMGXux=19@_Z8{!gjzB73;zNpU}X|DXwxK^;Cvj0Ph3u|D+PK~V7Z z?T_+HtO$qw$Y7Eiis5+%de#S_2Eg{NT?gs+rEQ*+9;JM`;i65mGIf65%GmAWA1&vF zlc?PlDec;zALdLmib;DC&8{{TV>uUmnkgCuNg83d=~K)66oA^Xl2_g3joQ7h45dDe zhrM9pl;y7z>d~B9=jQH;Q=2Fr{5!6n4(@U2+i4B!LnEVpkskhl8Y&h?h2<}2MvUa(Z=c-L0$s#VLm_n6MN={uuQNF?aO%NJt-w^*Q^v38n zSik;)49a!p_y;?PBm+2+r&6d%&w5wFcSS3i(Q0})76N`VU$9#xpY*=PpEvRJL*_v? zq`fJn6uibh+U?Oh=7TngAZ+QgfVq{*FP4XT@%T4DJXQ3^Q%|A#S*bgV=uQOkLs3B> zPb@_|qGW^GJGUz;Rdk=&!X5<@+IA_92osMhzl2w&pZpOkH2wg6{QNKJ_SprLV)J7~ zswn~v{%5cFd4Dchvot~B4Q=>*(PzriPyl!KvQ;DQT4Jwc7b z@=RK6_wy*9Ls}eOd#i_ifu-1gyG1I4B$wrf0s~uz`Oi=PUk3$X;9w*ytxP=~JW?)j ziGecB9d!at%>E`;fCYBIE`?LXQ%q2#KyT1)F3gKTVQ(^OFF_%e>U9C|Jftsp-L z-uBgv--?x$jQ!7JVOO%A6s_NIULK3t`AUvLNRGy1+2c=*hNLTgEU{(f`aS3R&0c#8 zJ)H~+lk7p>Antxg8%KDw8HA(zRyL7IsRXPZq(&|IG=anACS|u!&ze?(596{Wa^56I z(Hh0)W(B=vPMB&$-+voJG+fh`2n6^ zE<#-hLF2)fS!S>(AgaU7)DA<}B0gb;cUhr}#B$zitS3?I zQ2dfsjc&|!;>ZmeP`tUDacf0iky2%{sdnvR10i;nHt{`{s%AE_Ck=O!`CgKV{TxZt zvGG&6h(`32V2E)jIe5jAb7h61MnLCplX!amDU*7b478F^m0qqf96LN3N^S2xtX@WV zqjdFPUpJ(hHl4?SW`Rxi^WJaHe&^dS6OY9@unu!n*p3<-W-CQ>pb^E?XzN3;LFQ%}E-2`SgWHo)7f-p+JMy`RG3E&3PwN54o9wVP*Nq{9PKSNP@R_eO zKB~SbZXrKS%qqUV1h!p7JvFb&fbotnqw2Q5-wA7wlEq4H?+^~Js$F8pms&<$wDQtJ zl0cD0WH*i-3Lza6dDXZ-#eh8JlXkv(BGQT%ufa%jHyi2P_PS;2Q-5b!JPW(HoNzYg z2(g^gwcm)p-Q2=kK{=bNP4d6yB|A(BM{w}7e~-*Rt}#Z0uO{Xa=nY%!B|uW5EG{vg zbLt&cVKr)8e;2Fjx3r;i#5>@hs!6e6@JKF5xyGp+&#)QM4t?M}2m%79NOpKi>$f_G zEbVBL#9J#iY7hDnU;}~%>)&#&&6NL$+Y}5cc(#RW7pC-r5LDH|vnfahGt*C$(Ng4D z@UDxQAtvS2YmtXYUy%%-_Rv?oQ+J+2A0XduD3tbTMwumZ;T%JDNb|+ing}FNbj9t~ zYGxl7j3TfT+7h#O8vy*@Fq~5xnOT1>jYI=xJWjqnga#r=N9ytv{fvN2b{8`alWjGR zxGp9OJ=YMcpx>2RD*S{iX1{ua$G_fF-G`KzuP(cV`XlqHAo&r7f6owqz}@^MOA{#l z4KRTMsx;y;x}?Yp$|XFTGd=EXS28c9e09?>)%mkh%af}^xQtw8f2@dr7LZh@?Sq?> zcW-rMFZvfi!!af2oBTEFEzu_^TzVv`3!l41E93Syt^yVFVj~8=LJ2f0!YqbD6YAk7 zKmYI0w$QC~$@pI|ANU3a#__+FLk|4sGU%$9UxpGmYm!ka>h~0!kQyrg7CF?}ro^aJ zmM$&Bh_;6e_0pGtO6v>oyxjAmau&Zc6ua{CZ7e(q>9`2LS;159*^j)IQzPWhz;`GU zSQbg2d79#U7UBnOiXWtF-y{&tWCj$`AfDkme-Ah^Uq^Pvn8HXAc8;&8f&=E{f6Wa- z5m0=p;lR})#1J*jtIM;G5V4H*&_e`EX|Te(Bdh7$yW%)UbrRPWEnKA^LUWChkgd#q}YO& z-pbQge_K3HLX{vY(v8Ndy#VD-l=A-7^=uxXfF$iZecnnss~ZngOBXAjT?%fNp=jA@ zJ$hVjBu#m=2~kpYLW_odtK3bm|tv16fZEfF7}7vKNtrxO>y&HXNY zk@aEbvcNc!%FRn9e-n0v=&ZM~tIvl%zUWONu6EzU5^P=>J9d(xjqA&t-4RL^kT$9l zs!&!tAx2x}F{d&--V5*q=Tp4jlGPnDEu6(X`YCrSOJRNsR_>@G$&QqRv*Wj?Cm3z1 z+B)G{0Tpehdc0unLyH^!<{~%!Q{=gk$$^+9v)6?MC%xlIu!lE;cR}zfui*qpu zU^U+QL4`B4A|#i(N|ymR?a!s_^Ah%HmhZ7vH#H{U^TAxnUVzYX*gi{ZONznMsp>8G zlXqmIR+hA;1|j(3Gmj_!Y9i{2*2{s$HMiU;=fA^~lna|G zxh0n{QMbc&j`l3G^&pebs;Ioym)!V;h)pUY*1FX27P^te?Y!%E9}ie*`yK((+Qt;c zOz*W3T1(fUGu(h0!oCiP`+vo+kYS(m;!bZAY%lHmZ{}&ABjSMEp6dA==9@c;=AyCB z8OwPO@f*ZPn$4$P<42s$=c;(mxgY#To)~al#PN04wIJIxvGI~PN*cW*v1o!=EzemPx0zMa zZ;bBC-;*cnZ5Fu(CV*q;^X=o^R6(neD;u2-MbsJ?Kjh~J;wxUx7rv7sMa6 zyXZ?tB}`;n(PPqEne_ZKK8veIPl?3xc=X=iHCs{s?(J;=^q2zSXfX0of1;|Y8-6~E z0M@h~)kmZj8PSo0-SNBm`LprhHawiDmwzvb2zgeBF8{!X^8suvETN+W_L=@4d4A7W zmL_iFGYhIs30Q{ZoSWb6&XY11zMGy$g_^c`Ov>t1n{1aP5GW8ogd;NGaULmfMu9$U zn5j>t{)SjQJ1+Pv?+z~;{rmxa-^X3hY#TYbVk%`~;i=8x^iVpcOtAVRkk1PCE5}rj zt5jc=%`1}Gj}eF_ZP1&r$h2X$*+^*FdG3x&Gi4V-CsNcM+rCV8VyVMXNF&onDL7xn zm~~o?EWwUaEl48ZzDytdEG(h2YrjkwL#z^Apg=RlSF1_HqQhlN_Tu<^R!wgZ19c{V z!-Z~!9%J9k7vj3rc<76Wpe8%K$#2J_8wXpU6c-!0ObhVtB9GoK`}`z}t!-4)Pw>RM zRrO<3PDYzdenBPA`qhZcPNhL=bAxoLm+tI^15f7^8m8KqSoBc7ah`}LWWEl$;5w|Z z!Fx2Q9nGe0=oHdN$Dh=U_D!5*+(Q=AF8$albswx3DM9U%mt9ui3x8Vjn427Oh z<0Ww@!X21VEnjhmXtAxo*TzB>OL5f~);4jMi>wlV*nG6$5a4F#!a{oYr-{P633WH8 zOo-HD6*7Z>P`;2g|F=5pqqDjg{zlHLhxp4*3W>jE;t$s)8wQzC{a5al8z=UxphGwIEah$cFjbEH#H{9_a9S-93G65cv3RM3dFTa!q6L_9(KzDb zR4D*OJ-W&f98>?9*_xEntwV~W_#QtXHeUp4%z+|N4rz{$f!Ho3>#x|1Fw8Q z%=fgQR!p;CNSfpCY2p~9K;&t9EhPUP851Bk zAxxcpgugdR!_lo^8@F4?eV}dX(t=nzMgzQJD$PJUti3p`atbkJvzpu7M2?jRl)Gpg z`Mt!Bv6()f;+<$nKsW1Fg*r-L#@jo%1>343`}n$_$F&I53rk7WCmIj+TT{{hk- zJnV~qI@rH+1`7AlIdqexY%9jF z)q(f5rmv4Yxp^EzJjov|oph-da{!Yt_AAPS$BncKzSe_>+zr%w02^c^eL7W%OPO$* zIxc*nR2bh<^zNxhC%<{96w8ukobU|E!i#DkA~ALjvWNxaJTti7(fDhL%#7~3WY{lJ zo;a49@!Zfk;~wUYVtU9PNGs~?_p6uq)d%SD1B2auw;*cYGSQmKfW@YZNZmR;4Jx`{h%yy)dYQr zt@w6Sex+QF4u@e!9ym`89{(vWzH`&Vt=BnGZA8?Vl!`Iho3K=WF)bNpvza!9Zl5FAhzk;2?O~IOhJz<5C8nJx!boh5 zeRIU;CDx{3AT@eh@*O#VXla?V2=LBc8ls1(3V;3iTf-7)j^(bo?j#`WGJQJ1*h%Zx zR1(z_#qZ}b` z_j*zU3xpSIr`jU`rv4;!#F#3Ic28Ex?YG?cdl~o~OsS0ed2`_93i95wyaqr-xTQ1F zi-iZmY3XQQn#J~Uf8ur_&~4m9I=g$(Z?Ju{9V(Y}|C=9y47Xv4p|vcfMt38s;=AcR zOdh;-S~GdvzW^pn#99R8FWMGoD6qQ*@I_ zHlQZ@RhZSv-X{dsxwIrHRCz`ui+7lbs@cD{C_VlgiT^e~*;|O}1<wPnjA&`|P)rr>99aZ=5x4*D#;(U-K6`Ir zSOW`9F0mTS&-_LSviyZE1#Z>CDqwmO<|7sYp-M#Q0ScV_-$-%W%L0=Ave6)o@9Bk( zWNA)C<>JD8UmEQTIK~eNt)lkg=D6hJ_$}O{^@(;WwLXKRS zqNbV>!OFaoo@j?WLF|YU}0P}K=ani9qJHOnzwAt=SpT=*PFXmu! z@>E_*KCrDO2tO=SZ>=3aRZ3}CS(!g`S6py=36!ikbO&j_rE=8Wb=h$b&2!E!UAvc^ zm#;Q&`ua*bYL41mc`3ifN8b^p^?xtOF3*YR$jA^-9>dbhD1R&{r(#+7c0I{S5g z=KQz3NcG#+4rF>_tB~gFEW2c7yy2-9U}?L#=%44Cv*dAs;L)gw247*jb%W{n{8wg4 zscFt|SL*$ z2!y5c!8O>CSr?+T66REewdMc8fhWNc!Rm*(%x{a!32+ltu{XP_DXFe%&Yu`?t-NCNZ+qV9}-dF%ibhW-Soz?`vjqUhmlsD=_h5QZ*5NSf23 z65X)`bqx_5`3}McHHQVJ3&nB5x9%y=Em$X-!kxXqnMmRyS%uPx^e1Fv$;y=HCaMyq*Sl87b+d6}O1Nl@% z=bYi3;Uwi1%k;})v8!lR&D#NCUJMV=Vf~f!G4KJhMJx;+YC1E_BD07qEEA*27bo3# zxDA-UAzyx(BtWMeD>RAeQ@|VMg10YYn!9}dfc}NZ1)?AVtyD(ONh1$zqX;A5+U1w; z3?tcY4%;}5Un9Ri9j?V2k7Hi-taB>QMXbc zn*=$+py&qwtsNaePb6_b7%vDY4^0tSDGkb~C$*jdex$S>WlelM8T4xcn1E{ogkS@eKF9RDdr z!(#S($E?h#bMf@hY`cybuYL(a5Ul|nsxKj)^yPymlw^SYsN@^q6Rx5}KV^#dL?F`Y zRg@ZEsPd+YYfc*nqk@f6%o_UhZ!k=Hka@OIP$(GuwdR9CA!Etf89q7BHxg?bl*7wc z{10^B53n3#Ddppdu-pa~nV*NqP?4`#Z<_100^2fF>?+3eOSsSvo~n=)R*8c3gm6%@ z{}uM3J7sdtlrk9T+8`K1+qjA=yt3_9vj36Gkn2DA+TQX_$DYIb?l*a}{jnLd`JZD@ z02+8N)RwW>uK;Kl5HE{5*Jx5h<%^)f>xch;04K(x@3T}75BytBOP18+~=(K$L_!W=YNW`AE!kT z;I%`-C#H~$PRZN7i3B-0nB4KP0Cp)AVG`O>dG{_jMuR0imc8f=X35&qK1hGz4%!snx>1ehns-T$;(Ra~dbQoHeA_HbaKh9FN9am&FQFo%Xe&CVI;tzU^C{ft;na zLBGpdTXX27IT6dZN^`nfB=_sHH((L+RP56EFQ`cD%2(R_px^7XVte}=#kt$+JE zo-0ELBc_m%r;S!tLHULc_jJ&yUQ3j>;n{Mw9DR1_DYZ7`;{RmP0m-W3@^+ri=)XyA z$hHfna0MQg$_)mTHoP0JrIZR@=#zAWuV#oiq9vp1a$DX`!uTu68@SVOE5xe~3I6?6 zwoMv2oM!mx_!MK{Lwa(8rEOT|imtU55ndAPun8V7@XCBw1WCxnRD+sf_5A5GT@Brl zUg|~s?Wou9#L{udfOoZQhU8EMWp45fm@dDiuiTJr(6sxk2SvC0O(VAD&b{wLXBD4q z&az{kY@#)or8I}*R`$7s-egp5eW;*YLRx!C_GzhsLw07YNXt$vzE*VMauu(*mcmd4 zmOvyM^pRo0qA?t$Xr7E<5?u9q7XkQ?( zYG2z&Vese$XbawJ{M;i~%CucV{AKDjL;~7wPDm=Gx#5TVseJ?Ut~!|Vk`gR@#3Eq; zkr`U4#o#zntvFq!l+$rBX(v}`H(sp70TWjY(v{4H1G2GcMBDREz4N!Kw3+%)c%{i!h*p(&{7sNpJvXEtDDke+v+ zY_FQ1k#1x_SHxv!Uww2^KME;}pMlhxMrpVd}5U^`LCYO%}FbsToEL*RYo;N8`n(dSDq1I3tUMO@~a z(@B@qY*%b}eL^?ID4oo|a&RVDKiaMKf@ZT3$eJock;T-Kt-l?BT=3xT|q@lFWbbHS_56z5n)Bch5eqJpxnbtzY zVs9D;HPw@Qb666^N#V;H8D6P&IeQ*Gx!~N5;BoG3CWRia%$h`fzR6$2Q+|uTLf3qO zcFSj~_2h&Xc{&g;G=a|G*w;V2tLS1#&tyhUB{(f1!_t#KlKm9D3>ESO2UHqM8A=Ef zLQo9!FLY2UKdH8sLME=x6_1}D7~TAQxfi&L69V~f{12Tf7Qm)RRRKf84_pbuVce-d z_~ZLE2>-_S8xUZ|P%9B&#!+htA|Aj1)${`^yO0r-+7YH@tp$8p5twc;?~&{?(LrU1 zO$xz&eKZq6%RAlBw+mtk-Ea4^Vt+}bySUZAXBv0?$VSADU+T%w3cxeqihg{=(}*w5 z!iHk;C5WMR0a*`2VJDDF7_L+;>4<$`;e|#8+7{5X-U-QkV%+@WTG|#4vNW6qq}c>& z;HE1SY;GeybXCnDw5?|O~ws%h9 zTcL)6*gKU>Fmpg2eTAo%l~g*VrQxZeAsz~I*|o(kE)Z=2G@txgX@nDn%ptz3(!!e# z6HcihI|AkX_H>b?GuWsHMvDU=jiIlKh2N1`C3Czznu$EDrUG^-D3?g+PFfH;6y-GB zqRO5ru7^^{!hWLhGL=_60Go+Vaol48mz3Q z^qA}=JXt?(gbyvd82FIn2rlJ`{g3m|^`N%+BEDwEx+jrOlK-1ptRp5<`a}FTr}rNU1pl7_E`S*pkacqRFm-Scx3M(0{~v^r zmTIVsA&MEkXWL=ey(7jHNLuVKuTQTJpN%?-D;rBK$-=65cH?xuV%zM3&wId7w?+_|O6p*gRmO4r*v=cWXsJ0ccK=*WD>+833#iZTs#T!E zs7%whGkVZp^I3n}vjaISpmwqQrrqH0zai`O86%C;DWnEFXzE%NVrQ-}>#)=?Bm9+x zcKm-D7PXhlqZeL|%0AAo`85Wd4u7>ePbUO=fy%X6g^R$gb~@AbiTrDq%s;m@N;|fK zmYLTfh&I(?R{9ahnuO)S2QOF$yfE?W){$23*SKo@Oim=u_g3qvgPJr5HKXL>WPX;N z7Lr2PJwKA691y|Jgz>ElIpH=5@jX7FsOC1+0zAK4F0R|Q3hGZZ??ASblTkYzrbnq7 z0PLpZmO~wXeE%*k;ou`ypa!WmR_;nfZyjj~##gusHhez1DR zqjpA3d=npHwp7I*uY8vYe8tr3cZojB0FbH0sRqi6n(!#s8KpLI#b%+tD;y#hTA|M_ zD{v7MkqEvv&bZ_M?$h{WXx*D{Q=TuT@gUng@@yKnr-#}r0T7dp+0%&!IW&=cv?gMb zuGVFZ=Z*w(ajmE#M%*)hl2WsOpg1)8fX6_NEYw6@dwcaVe8x{$9;TwRcyjetFG!SMDs#8nqkHnj& zm<~xPxe>|!{c)G*Q8;PcaU6aDNvWm|a$ek`Lvp$7i$i*qKE%7y`9`&C%h(n~uiyZG zskwEc-K*hZE7Un?x9rv_ZjY$}2kP8EP&tw7E)3rov-H?-(!5$}-WM5XFUjV#j}yr=5q6egj--@?H(CQu=6@ z)H6!6r_))WZ`Q92)G&69pcb1`3i^o}C~`E-(JvsAK5sNck_tzHZYfMy$~}T)xY#?W zZS#&6*I=fm&6 z>UNR;)sCb99fw1Zfv>4bv8%h{pr7P(YF7^D33q_g;f=eHinkx2@M%-rvecSs#X(&= zTdg#0laQ?`n7**%sHYichsq9l6_xM9VcN?6%ZtK6CxbXcvm2?W<{SB#Uda#$sNV`@ z>f*@c*tv9!DNjz4|Mi$usk^jlMV*op+gW5$<94J148fV48e>FBU$!Y+(}58BcJ)$H zVhp=OCiOFHxU;A^r4Fss=~wOawh$4cVbC3=JR(dbkNJ1b+j_`vwiVXWh>XSGOmZyo z+q;;PTeGyf>>8IqLq$YMv#FNAdXj{{XVuYzOtG8;dA-dvku|-brPh2U(X@WjYO23; zN3jA1(Ua>^{bqj~IAvHDTKojm6iR>)+$Fe^E*7t(4OiRi5#z-9|jZ9c!Aa|&I{qM>0Rr(JA>&WkKCN-QZ z3uKKmTZYre=imJnNP?XCmxDoUP?L-iqKgjlx@bKOb{O+;HuW(c*|G$^0z?oYLzmS^ zw|`UP(iAAD7gjf6t_j))Igl@j;4;hOlB%_2$>W{c-RdLP*%4nty-CmBXeiJk>K_eqEFle zEl#OaykO)Dq$pfOZcmGW2T$u@Y5}{$>?E@W!@Aq?h!us126P6xSwo}mT1_eR@e`|N z@k{$qCBKyLRH4&cCncur*fm9Bx&3;6acwzhQv_9p$X4QejjPuKe}qI4WN5C4Wvdq` zbV_*_@whKj!$xuPLf3HZ!DwZd>aU@n9N6};m!c(;Wuw4G_HCS0IFuWCn6|EeOgZe? z;a@3zSKPdcO3fRs(en)$ipFcNgY8wN6uvokk|dvFJHcikv+d%-isH*{j9SDqhqD+V zL_^MLQSITo060qkvUsXG4er={`R{|^YKG+4?1z!UL=tceM4tG@2q{v@{1mPZ=JPA+ zYTXESRLP3rV9o|Tc$`!_ddyGYMd=DvSI}yQ4D+kdo{Sg+LgpR%`8QyH@jvjHl}4YX z3U9OOUDGeX3-CJX`fD*#gV@^Ob!&~JDC-6xHweiFlTDie-U{RIC5_Rr&Cza|E92^H z>^Yl)a*WPBbpK-7xl`z4#_IoyBnuba(txkDOL!YAm7D459A*!0Te=s1YXMkG^d`xqC?6-o0^YiK5~QMaLQczA9`L$jQgZosC@1X9JVtyT<9 zUVC>Yk%JcAZd8;4bic}khi@$L+PU|GUmkHGjHhpw(ZadkL!*-RytKy~YJg5fApZP0 zem^oofz}FrO8we7eYai(gKfbW_t`t$Zo_@Wt5h5yOhE$U(I4f!`r6{pZa2{(^3Tll zi8s&rK)*<=K0NaI1c@_^*59K)PB@`(j_4PhnahuQe||vpl;tkNYKgGt`!g)UDy)YL%}G%NjT6nDJ@O8hz6dV7o?bAc$IY2}I1GXrt@ z?=@4Ypkm82@CV8A>lQ1W_f=vu&0@KmAI}1Cz{R<3I?#3H9(^==i~VCOjoRuVtS46f zmrIT9*l;`AMLId@HbzqqHum_+`9O5o74xu^c{onz>L)6WNO&0pymYe47W&2D@2l@r4mzkzc`!lDZ3e!+ox^e?CL~*ORHGP5Z0#zT2&dRU zr|Giw%E6(9t3Zm%u$tji;!@tDrGB?kt(FmZj!PW<(-`8}J5fK{<1g0!_VPn7N-L`i zRJiU46)Z&SJ^bnKZ2;CaivXqE+0^c?5<7_4h5w{4rxEnXPbBf6%LJdZGza zyCMe_@(BJCGkXjZ!PW3FzMkUX3s>CVAL2448Q@BfR@@@+{hVO2eQ%y^xTyj7zLJ5k z1L6vy<=3@$f;?dQr?~7NJ+$)&>(9Pf09E=k=_|GACbL=bbdB=yLw8%iy%mEiq4Ko+ zclp6KS<{#C2obPyPV%6f_cdk=0k53%-vRn+GCL7#Ik(zN2QwWJS0dujhbgW>L}MjnFelrnhW`3*o|5~4t-eY@qd z>0JN)R`@`<#&1+uYk1Sv)2`tZtG06$&eVp(M>z4iSsX>_`+jvEd6S+x<*D{L!B|x< zJiZl$G~6K)Muk+5dv_$TV(U%kFr972&kH|CTSXvW(8p8F)8yrJ49=gFBpyR~VZOtq zRQHM8Mp2ovglp9^t_Q4ZzB~Nt*RgwYHyGu6ywBst+d#PR-JfK`o_^b4y0piDBOo*J za26w5bs$J*BF?1zZB&vJT|(Q)g@2ZH70AF&NTnN)UOJarGNEjU^AiO32W`@oin%>C z2J!TBXi|x@Zc>87G6(&-r2Kd+X5+%*-PO&uZMQ3W3I=Mt5)F{8pI&ZntXM#n$n(7O z6K7<@8(PM@l^|@hT~4yHi<%CLiViQ;(Hr^YxqNe#xN0upuuQa$sNry8aaWuR#d(MA znf>o~Xs!3yjmlfPye}krTihRd`(L(Xpqa4D(h0?^t>N5kq@HX!M2y8K+IvAaeHUNt z={(JH6}5_Wb$DQTMpOSRbPdz(G5L&8SN^FeJDxYoS-$&+bv7U;Uq9>O=4G>?bIk1G z=l&#JnH#i1pTkM*o4ATJ31o4)*&3|PqXt=BpTuLBbc^nYQ4=9{8BK@Dx%F}0i8-ic zByFcQ&b(FPh3KOq935FTcx?9ef_$_+v=^^MVkzImGi8R;t`-8(4 zBYRTO@_AmO_gLFcd^eE3@@euY)=v11CiFdoqpXba80D3IiUFpwv7lT?M$$VzxdoFi zJ;)u}qOKIL6*ZYf&CSV0YkI0H-KkJnl$@ll_yc&bb%9&_-i`M3XySwy5bhLi#a?)7 zeePbEEzf?A-TQj3HS=V4;+Pq7)LDYE7uOFa^@O9qFIS`(!qHde|HFy{q~&u@v(y2x z(l6$`TgTDz{rI9Hi=j7cS3mqy5A6;FUvyj>BL1`bvSI^9w&7`7e&S0+QaDfdim23O z8VvYV^#sy-LHHoMZrZX{6+#N@4f`x3;gNH%X-iyHwgx$u+>-4bOMY-TTTjp!j`BC$ z+z%GfSaiL5i%rOSaOEL@&z0dnKG3#Y6^gYIsnlR#qKTZEb^4&>$*Ss!u;G4>2VvJ0 zQCjJ0B%FSeQ^k0kSNc{p*8?ax#`nh%8XHHM3OCfl$7hT2fHf-8uEy@Tjy5Q^HZbzVa` zvso)Xn7Xp1y3U1Sz+CKiF0_6rpaTS=mKeQZk9k_^;`NZ2oAt;Z^D3Ff#VZOc-JA5G zS%JX#c&uK@(lMo1G=&s6EwLb5OE>lD$hse>^$=T`w{#l~)Zx>)JA4+Jin~U&H?|>` zqlZ@dMfEn&?~vvn zt?eVYUdVVhwM}2ES}w>T3?nwIf6F!=>JXgwM$1%81aS%)XRweETO z{}w3VGg7Q!Wfi8O#@ONle+Y+1Ss}~|Zh-$bldVWN{4#&&Y;hd;5lHnWzRoo(D6%^o zqOq)IbQ2F=y)mK~qOo=Ov*3@O0QANFW3cZFVZHI5fXFE?$RF~K#|=;!2GvubB`BhbwiL_3(~Jt!=5NJG-b8}gp`#*Pp)v`M72u;IEg4pBH)7;IyWO^@&H56Z&< z7aT=NKayHO*nc|-dG`P=Ein|-PsNoVx=bc*7_8l}IvbGA22#QU?=*wws!(UEpLDgWk}V>hc&i3-`scPPeoect z59)7t{_aRN1w{oV&cXu!5Cv-nK2@+GQK}lHL=g}_#De-zD}4cGgePBksPIN7(j)Wt z6(9W5W zh4o(*#dXZ_J@Fmk)RIVQ<8KXJ7s1AsRJ>zr)O}EcOG`KjO|k2u`Vsm+!+N?do{3a1d&Q?oh&GX2#w=Sc@qzxkjYZo%Q}zH zBzP$gte#v;LuhjDZ>?vNMt(8AWumrP;;hh&I>(RxF&6H0p9=p zrVoMSx@hSbW8c-5-8smUlIfd?Rj#=}gsLGgZ$-68x;j{HZZkC)Kfk5oj}ZE$Q$2qH zlcSSafoIFz&AftXSDMBl44>j0w)MPcxL8q;2Rpt~YyHOqul$oIU-$1_8x_ar4RFn44%w%P;yIVb9ef-7}0iV__Wz7o;!E>}S zoaxaqaj|bsGnk?tcIg^)29X}^i-en1Xw%D%Chn#sDLmn(yMHKt*nH#;(v1O}gRE-l zNj!FY8likgX^GzhdF$_Pav7>zSEK4^Oq6IB=)>RiH zy!TV-XP=UVNTNWx2$mjn>zDzw@5aP%Z1iHpDd3blqoAL%<0{< zefvLMTy<1bU)P2Kq`QYf>23s(mhKK|X^`#^7)qq;BGO1pcSuNgGo*A#gP9Si-|y|DEN(ofamDx=H@h3gP&^`Dxi~>F zz;(*HaHsO^{ymGm>C`-PbmCl*U<$2KD(>SCDs?;V-Y?)(&IB9;1crx=Y0*(a=trGB zD8&r1h`A!zN7y)b9-ZG)EkoQwz99`kIXxw5o+qNC#>iwx=e&{CsizuKDMZ+b6G`+rLLIRzc1f_leG8 zvqD@L%3a!qfE>%I+V(3_)000>pqyFwrV8;@V?rc~o@6-VbM)a&or~$h_7Rs&p&{Nn zU5qF4=-FoP)rCp>is*&o#^naqYuT2GPG4q;ahjrWo}A={bB14z2)Qeqy)Zk9>PJ9po=#Q`NPHZ1QGo9&CYrSnF>Pou5!pH3>U zyb5J_Zd5ytZW9+%frh3;j-mlQNS$=|m}TD4a+4qYsMRpOrAwr_S>H}xHOFTr!egG& zn`F)6(XGYLuf@w(Ie)M-SjuCYX0a=7UuoMgtEqL=cKSN1zRPzheQ=Rgf0CPcRz&E! zLMN`Bb`4T{<4AP87Z?@@tq4Pe6zB5qL2{q~@V4b*Qq{)`>A z;ffhp7`u;5N%!hAMwso&U({Dk{c_gTt7j|tQdpn+b^#P7La#U~RA}W?P}6eHaQnt_ zczfTzMVMKf>e*kf92KYS8Ei38>S4ZDBqR>>Q1(*$%lA{}C6=4bf^D{?%|F6KKDSH~ zFbPV8neFNZlXl~;5*pP*HHR@%{UtiqjrbMMb5|xAPOw>!@WqIz@Q>-}N0kQ#?hxM^ zh9m5x;BbIrQ+0iSNT{k_%x`pZLT|Y~@(kirT5{W)*L{GuLLbYvrEnzM^3n1DPe8D) z#g_VKgOw4psYwNtnWR(A*(>q@l~?kEmnfACCyM0lW_#MLG;7n)zns2(m-XSR1DEUp zj2jm`+gz%oqUix@JLjJK(#EiK5Bu6$k?7JM@0082dXI3lc-^%m)_P1D9^-nC`H}*qm!av+;V-%t z5|+zZiR$P^*t6j}r8liJ)}O0u>m0!^noOGU5At6iCcu>e+;qumP`rM%ce}a@DPO3u z!M<}qX>QEaq1i4;i8G-)+7}CxitjM}hHGYONPB!>pQ9HH{^IH7yclB=Sqb#SS_=`t zMtqj5O|emTcT(Yz7%9~xUBBg3TIf7~=6%e<%FWf%HWI0o3I zYkbGNPMh@0+#>TzM4TFJ^7nn-YpTDQM7h#zlMCi_oaVjfR;^D{kEu!g}&Js96;>vsD4% z!cTn2>BKDIi%+0YZ8 z7o^FZhM3qgy%geo7jSp?i@1YIhweG;l$@lN z1SSoE8QGZ`+J!*a%VW&ZFUYanv8a$ug4UEIs&(pq+F0f%aaRiL$hlb1W%=a+Y1gof zQPu<{;~2WLa(2C825n`%l9qe2+FHmgL&HgmfuR>8 z;EJWyl_SuWYCepitN9d)E(uhWr`4DiHYjV)2@qhF|M~7ItpHRRpE11HnscS&wEH?x zV*5p(!62QB zo9M_Uv*ah(3|I6^0-p+pxA12r^)tcJV!x(HyWn{m`kK6u_bexrGeoz13@Mr7TKWYB zuk7Tpn8VhgCDr<7H6kiULt(Bwg>NG}Ye}(xd~+koOhazK|B;$8$n;*~&2t4kK`lws zvjxj$^O7qx?T=ropoAcnoeVRcvn0=GEnmsOln>U5(vaclMwQS%4H}g%Ke)0v2-cJQ zlu-7s)Tw(mcJYn|s*1$H-*oT6yF*su`OT8*{gbhg}e!%ab?AoKYMVjYC77z{yS}>qXrz!7P z*Eu^B@Qn*J<5i-sxJ+P;6$M$(ve@);>QK8f9yhLbk#$(66%9J@iqs0qyM}D1JED7` zgtiB%^l*VrzeQ5xoX$t$dz|t_nSMX&0*%Tyo}oU}DKAZeYp4A;LFmy@%7i!Yo6Q60 z2$X@kE^6W3#g=b1)l3N%%2QCSJt>m+i*U0`pSM*^G>)JkU3!w?3J}kHsV<0RgM9X(rx5W>+=Z-DdJ~cTk#jVgQ`zFmTp#~>xKR7|s7R#r_II{P020@S4?HU7r^wif zJYiJ>2>`XJo(##S?xx^U$g{{%jQ$d}76wUZpGPbO_0m=o{U*O?B6pxiY-=E#ha(95UCF@a&(zwOsyIlw3*|vCXbr?pV@5{YN>6ZjA@4d>@zHpxtyH z>QOY$^umFMsZm+8ajxWTTLthvmvg{dSCYu~wUFA8go-sA7E-dFyVfGJuqW2=)@7*a zgu%OSyA#v~2EdiHTx{!IHwgb6-D~u%~l=xIcY{e$O~ZzYU8F zV#0C&mAoZhHWgUKfDI?|OA(*ZDo$5Bi2Em_*7^T69%tD`|6F zRf_dABa#a^1fD@grvvt$?z`$<{_W1L`_mo>{d(X2MUk?f#cWy#E~C*)gRkCdODrWm z?aI}v++t9NJ5@%PC`KJGSLlg<6Z8kMRdQ3_rEhz(p9If}^n_zDY%ltZTLIdzUhyS4 zF?t;-!%6=Z6XO58^j*BdAkm`qs?3Hga#o($Ij=VYC;pHE?bOed^B%@;vhKL9%<_xQ z!Dk<>-;ps%t17f_Xfda7h{{@!hH(DDV=s`+*VT6taYG_dTc!Q_13iCWo2i02#`diOuVZ{rd%|YCfJ6~3 z705b0heS>{H??J{8tM4@y(#~Wpo%xk-`JP+9oB~Zkl!5d%<2O%kLSMbes2oBur-zr z|Mn)i3zJIacN5+97F*&p&N!N80-jWM>yt?oYZuhq?6D1V=0HxHJB`G9M3h?O_w68T zzeA0&33$CA13m(R2r%hS2b_I?Ku2Hic@e@@irV-`^I?dJ2`thsQoD)nLBT>gcG6{a z(&Z$q99V<#IQhIDR#U+g$1UNJa_Y{KE~LU5Woy1mxc6Z@moK~p_S<-Ydb9(5_@AF0k{nPi+zDx9Zh+c|KvNFv4NrY0Hmb9EM#ssaq(arJ_P@Z5!^ss2@ zdA2-|!DUk9n<@|kn+!NnJ?h;REO~9{OP@0`Esxnei#f&dX8K>trD#;L(@wOfW&?jP zmV!U{_(*l-`Q4J4h#3blRvC2xO4muD@K<5l&#xsbOjFw`98%=b$MG$WkkR}-(+VBE z@}KulQU)b+468KIIj|>8K@B#T^9s7bkm(VrPp11XY#Z_xqZp@5nDPG5qp=BM7pqFn z6Q4q=5F!|9xP#*5h9J6b9_ZtQ^_3EwNXThX2ZD&%+LW^zwhc8kcD4Lv_4!7$GgFoV z9Lpas!19`IFn(@h;UB&Q_nA{87K(4YC~6ICQ^FP*oIeMI8M7W2LpNemQ%|w|K{+_A zuVyoQnMC$FW19U-8@Q$8OE_373a+0ouKh$Hb4A5+)jkKqz})`j3_kb2HZX`7=*I_> z7aSR3Aa&FEp0vgNER{;t|D{Lx#hY6G!#0ikT#h1$eW4_5ji&DptByD$@_4 zq$mM@?{^Gc4lRw1lkJU$hIx$jee}kLF)F%kovA)t=-Ucam^eAVDgEu7_L7pwFydqD zAyG9ObHY=cY0?-@l5j$TWQTpOK<-~x=~9PLh5!`wBQGJI%wrhcXpLD_fkT*wy= z+=_G!_sVM{jdFvH>0)$6FD;m>w(eqXXblCWp_Q<5F3_eC?-GjM7HM&eD1I zs+wi3^G<3ngJdPjNr=ZlLs(2`mf8!w2C&%sT`TlT=J^nH6r)|ODpEV5)>uA*6}+bW zFO4nO{W*ree!qt*;plg^20PFCJaaj!9+Of>`FmOz+DOzI<3-dOwTywYCW7+QjqZCh zjCt-ec(}%M8h?4VX!M3kRPBV?;2vKzYs;hEkjSqK=bk8A{?bsKT}K!LXT7SUzc-Zdr}IX~(^WGTuqsS(XMhkBlB zMb2@nwg!Q#aY@5(U(>Ag%!Jlv^{9!{Q=NUJ4f}eW()U|^>dTfrV zH(u}SsY|W|dXpv!h^Mv3>AT=LY)HCC#tCDV`0wdq`c`4g0gk165Q#w)%soFOK_rJ4 z-rtcF<+7fK)yi^b)5igBT#^|)xtZ|IyI0Df$c~qJi=8?Eog_xhHP|rc9r5y zwE8J#TVg=B%c)QR0d!5*rR%qDl3z{KuZHvu!^q98uTO`x#>NSQa2KnP>|8YCQ84jh zGq)J$Mj6#P)|1=S-3TJR1lkF-Y#N`e8-15jVqTzR;{RPYcBD2EyDQUE7Iq998)xXA_> z4zqx?_#Z%-!_Od(h>(xQ6n*gkf^y&jH^X?4|0OEGYrg+;22p7mt_rZ-(zhOU`)e*z#^b9^9M6qhZ3k9WdSAIJh&&LQlJF8e@s+BV@v>a=nkA%(*tPZ5MXo+ z2c+ZysM)Z>T^7(s58(N@5U9rka2YoOsd~dtf$qy0^gPXK~)g&q8zq=_22ttppo$aO6XXeu@V2pBF<+1O(wndEa6lK)Zny4|&y7U=UH_L+E6R5Ata3_$aS833vsw z1)ZcnV8>z7pr2X5t2AanY+4+2mIDM$n}d)G9wN9iLLkH0$G1_KWJsQ>j};n6?p>kbBp_A`>G WDWbsF$p{Gi@ZUasP|4|kdH)CXgbPdn delta 19998 zcmZ6SV|Snp6Qnb-ZQHhO+qSKV?ul)4V%wTbY}*stcJ?{%*)O~2^l#{{zN%_q8mzYw zte)-%Lgkv}Di{O^$QcX>2t#s#8D_HL4|IUh%-+P!Eml)c3r!3CD=yRA7$3q+I5;Yp z3zadlWm&VnS@sX{4~8H1;v0x#Br%GX^J9Z@*I2%vP(4p2N(NQ_FwM2=ODkW|U(td# z&zWPws6kcq%b9HN7aPx){!a(jR)2*coMDBiBld!Ve#nn|%MD9F{An-VVXdXk=+^)m zAr;&NAw8QxNkY&lSaEfKRgy(BxOm5d~Z8G`p-x_6-tcR!1 zj|#7__x>=ZY-$wsCrqv?vKY8O1dRa;&jf$;j}+g69J(;l4K3XV#ydOrU9ECR^ilM} z%pyxB2|n}kI6bN|raR+IFh=|%P0E;XD2bl$=5k3TRyQOwMQ+6m8{|?Zt}M;M6u%!T zuauvDZn(aJdCf1tX)RTXd2l=`v$e7`CRKaTah2TRD>zRM18BkP z-i7_W1UOzA8PsF->Z{aMFTw!5)Xr#mxwDFf3(_-<#aU*GQDKVCNK)s;pJ;t`{$8iuC5<%0GZFD2O9AeVZzYhjVrcW%dxWrx~c6pNn(26n!?4dCC~&c!-KvZWBl zJQ-RzWmj9Uj!Gle#T##Zh{G_1M{x`X-@C9n1gh+STV z^_AnH+red%76@YkUFAHkja7Pw2ALk~S#kLDJpc60H~S){Z$tLi%IG9L3H8P9b{2Rk zJxEzRaY9>LeHX@3bJC8IOmk80s_4_r$;V;vYsb_?1sSi?s03gn&y#<5E2vqr?)f zXKd*H?uq04)i@AZxV47+6eF>RA{k`O$S!~F>oi#M7ulD7GC&L|SX%Kei7!x5_nrFX zN52d5z{8wSY=C~h3BB-uL%(i5TH*(WP@m78DOU^%67mSODmc05U%dHdxWpldoIyGC zL-v}o8`eNfL8X0+d0w@$ej(q~X+ts@p;b3n$_ea*IR>C;O%S;cjZ2}QPC-M4u8 zS#hHf>pi3!DV*z+AOv=aXA`TVZMSIwFUO;m>uaGOnn1H^Y*Aw^~{qBecUcYD-L=jfNYP4rJ}f_L+iV!PnszDE12D1e2Q z7A^A(KB&7{iaMU-l8ZW5_!~s%&Lu=78vgYj71u33sOS+v_E(n4@&$Wn<>eLj)&_Qr&Rq zD{B2Du?W*I#UC~7U@GI3a5!)A&p|{kFqVP>ApH6z9Fg>{{&#dyS^8H{sMp;G zB*Wbf7;OV2}L?_A@AKi+yK zuXsy+oACrb;AL=cc1g5-P@ zDj-(}#!r7l=Np*6>M2`V*nRBiX;i$>Ubf+jBbbOplj|{`NUBaf828-cmrsoXwAOtVY6|x(sgXW6 zVs|>qb~@_%W@~!gY%_d=|CM{UOuW3m0tB7(Syioe6=bcb-=9~$B5=I(p#8-eblPo0 z@Dq$64xozoH*^hg3m;&_0pxpsDRThmgNPpuflSyh$;4^(GeO>jM(PVjs#CwS zU!sY(t5PyKlr}LBCKwIQ+~;*eCb_2a7esn1=i8|e@StCS7m*xO>wE;huQX2WI55~ zI%bJBy-CPdFqh0D8zH~n>ZpBu$o`@?EzgtTlF>jmKxHrCjj%J#R5g>XAzjK;bsA>{ zQ^H1t9e33+8JBH2rxnx0YaC7i>S^o{bgahTh{Mc-Y48*}Brfp^C>zI8^b|U#Ql?7n zSq?qbTC?W!Iae*Ei%1ketLPG)H>cZkWqD{s%4ZY|^LP@TD04%w@LK*9)0N|0@N6&m zRvvH87JON2IU%ie&TL>^wzlVHSV#Lf(z7%uDKBKo7xVM&BCOpuo5?l-`K@(-pQXPG ztRM7`RUAnZYGn`YL_9`zb_c@WW+b{4i7LTyrC|q?(a;bNYt9ur(Hzif1u(tV89SaH zn)h2h&Sj!lxUU+@@ZZw^kc=n{CBcY%HfQHJ=c-rorQPL(te2H+3PL5Pquv$^EVup2 z<%7D4qcGhL5Rn={#ii#2{8=nE5_(rM@r#l?wi-eflJjs~Hh=h%Ur`@ZNL{`pTn;aC zOFjHdW_be!RB6?Q4wAC`xsG~t*p}ld(e@i6o6qUx5iXy`A&1n_9xvwLs4h-(IF7Ux zt9R1EE_z@_?C>tG$7LcZHV{Yl;?j&)&CFyuO66$in#?CI6GhX_ zSqFP>-IKK;$L%nDiih)#etorD`kL8_JXe7*ROuD)AJRU4`WEs-nTTh}(n^nfvd_5d zicUYb6ixfH&FSxXmNVt)NG6ZX4oHFRDMYQ;_Net*8kC83Y3?Ff4O-<)dEX!n2sfXF zZTIz}1p?ow1q>E|(MTubQg%`acivRGio_wzp36L(gs;MBoX`t$E5mpn)W}KiM2VN& za+DxN;kVan#p+4Fw<8^1?T}=7FN74FS(rXg3mr=yd1=fljn#9lSfq-3iI@0zFtj=?~d)hqQ#j+|`8#(wZZG zX}cz-3kE99OnX@bOFr4e^jRSWE^F5#cu}KVeT;-aR@_D&oA%9M%^{eoZR?Z1C|MTI zlmZilfi4>Dnxa*ev4q$fK~NOu0r@bxu9g)PkG4LikVZa4QU(1lO$xQ4L9i?8WPWUg z(k&IKRBShZ@AqnrEfHM$ZMiLB(+;Uc-@s2enkMmDUV5(a7i~9;-2?qf`&RTFT32Mkhv&s&SPg8N z`U>;|rjyips_#U~3gHyFuCx8&HzsgQCUK0)QEk@1Z#`FOL_JsWxI2B_eh|6NgA9t1 zl8pqkvZ8zRlH4+y4n&q#WoJ;9@HD2d@vhFb zM~yXs9j!Sz9acuPAi6TdhiCUk{7CrH4C}-qFff0VSlmR_)d+GXUdKU2<&6}!@gh>z zcz6^hoG~)DkZ4k=W-u}{{)o+0Y2Djq$+ta37BL37A#IgJcM;>}RGsocimlZFo&?=L z^^m;t4ehnF!kPkyxiWA<@$uTIYMOcJaA|`;=&N$wa;vI+cZ=9S3I&Ww1>|vGxbWZn zX@<?f!J5&Te={7}6-8 zj>kLoZV&P_Y&!vK-&QWROXQSOe}7zt>?24+%@#z$>??Q__kgAVLfr>~mnkGJ6d5jBxskF};FNu^~7tUP5k zeLw)CeIjkLoOV%o*@p$nPSY_ZxT^EQ**4FVT&+e29idT6w3Va2W+TaVBPojAUgmP) z+kx&(_pY8_l%7Uy*8mF6D-%JEWEBz6JbLomI=l&sFt~~-dp(R_GL@G`Z@|KG^O6aI zm+u^tTa#Pq+>45zCg*>5RVmj>6X=w^cM9_oldZC(L5{b{f2QgR&D$Tbt+cA zX%Yavsbx8pDPb4orSs6NeV==DGNQd_dIu`@w=ITfCdI{}Vph>__y>YA5Uzvd zgV!DS!ULEGzTnq&9rF`YE}3>(pE~dE!?KW8{(KZFcFyd3bY6J)X#h9aI^NNR7)t44{$n#`(eRD>Ci}E)@7%oWr9#=DA)= z%+7E?X-@OEY>c05L%JNzQzMNA$&xqfwOC1c^K|V^bYz)zvJusDRe9%FtQ~wcSN%XQ z8vvQdaT5SGgX6s|{5KE{ndorSJeF~YBI_LQq+Lb+rq?x_#S$`aSYjSk2n`{xPDmTLT#?_2s!UgvwF?Vy=sz^7K!fk=UKRHMhI$k5xUx(kRO49rECHB{`x)uJa;EAIRo4^QbzLq_+9$ zKZ6s=^i=_vi{x^rDwqpq^yG(iO~6AhuImTrL|f8k8;dPb3EorEo7{_qq;rzs^gN;2 zV%?s^(;Eybk(rXo(>{ceQ0?b99rPi9|2sc!d_bYRUFJ5GmrDnBMO{|P=}!L^Lz>*0 zHr<>#o3A+UNE*UT$~q%_F>=P<~BiHXwZ3!qBAr*2BM04?IZ;leGl*PJ!Ld|DER*^~lvH zAW>A^bepL2H?C(m;p}>z+IkqF`NkF8+Sxu*Y`GFKyROq22-~;+oC%T8*9r3iIWInR zlT`@VoJkW6uRf8rrCGChoq?Hs4{Vdh4gcc@$YNb8Nt$~`rq35+&BNHa!X|0w6qoI%8l85Ex_-5YqpF6XA8J*uG#{mDL}!97qmq!IS+!TI z{8d;U0XtszMGznedUij3;mDcoVE<|I@7|aH`rW_hpVw0h@b`xFmx8w)4xSjNltps# zRI$DM8h*41z*dT`%~GDBX*_~Fkdnjgnxb`!vexBVLX4-xDY1qhPZEsAk~2ty@jRXy z|KC)+w5z|0!$0pPyB?}dy|4?CL0qLT%y8~A3$Dbt_!)85PKX@Dm&2GCLV;I~Z;&X}KQs{uK_O^H&>7_K|_sjCk199Gbh^ZBAZu zF^KI%J+OSX=dtFdSzhIp2a;I?HagCty^BYlfJn-f|IqIl7mf2))I|ja^$-yvohe$S!>oC14N2_?n!G`$e z(mVP8TyKu;+j|JvC7h=+$6udkr7!BV8~^!}gMEcNgjcLuw~++c1D6+8}c;PFX| z+Ao$85wd+)S`fR>@muG1)GkK8ZG~L!a4MNkNrg5TxdmUxB79TtalMJ-P0fWvYRsn8 z4HFPx70CDGs~d^TqYt z$3)Pp*BIbj>n7UZcrXqR%UvxoLF!S`YpG@b0Qm&fT1h@%F0`>g&>BFxB|}i!WgpnM zl(+HLoqpaK!3_xdZR;(`DU@s{G|~jXPFs5;&cKOx-glncyo7EFM(g<0fM*T!6%Qo^ zx#1o;8xFv==kKKB283d9bcdvKeBl0_yMYa;+Vz_6uWHZUJYl0BNIpBjsateWnw!18 zg@OPUZ*aegcRfCI28?dBV7Z8iGZ)U$YwW`>y$K}V4cY#Q9JzZV^35^iBjNx)eGR_W zj|e{txo)`-fb=h?WUpqQ3i^V}w*F!oN`?YL<<5~qZ+qge|{Y~8_~{BpvIq4y&G>*Y$ZuY0r(8}hfc z;=#17))kWiw3T^i^f3CrtU$vSX%$!CS=sG8o`pHXN4L2eu)c{8>4X29R=ZW2-b)`eO&3*Pc3uz-@GwkA2x7piV_5H0L~H9f6sGatn$7#nN8g_2fSHly z>sQ=+CXtB00;_VDdOWyNXy{K|lq)l$TFkPi(G$G8l}M1mkMWT%mJ8GaS*QbGz&WTc-FZH$1hKn{O&DQcR5@Wl-e zI}}?@NLnl1YD)bFzEEX5F0IKB{Bku@fdk~FKC&yzYP&0*6}V+ zHNL(;a0SI@v)1QB$o?*BEn)KV@l9T%wO$UW0foL;0jefMc2&u%_Y41W2r?4XaxFns zZ`Oc^z!&51>pVc3-<9whBcqRz$LDwNgtBj;hhlA6vUiFV%xnt5P?4K9pXZwpQ!0a$ zYAGr!$vcAvs%Wbb_9TM@Can zT2WA3Gmk>ekV0#lSn5k;%4?Qt+4#41_$O)PhB%WWmKeA6gbhpBk6RGPp(bwPypaTN zh=Dy1d{igXMXOyD`l2np8xc#9jI`x_&$zc+LwE6S`st> zJNzBGZ3fHxkFvgt8aHiP_nDRA3Q-l5Mo6OfgVtm}Gc2yZy4%d1(8QnnO)MxRlsWvbQH714?d)X5 zI5bn#Hj-9A(O9Boj9;9G8p$y&|Fq=CnVF-jTV70T`tbe{48Ka2jAP!U+NL|0QtEKk zjf^Ai#De+P7_5?)OHVf84i4;$`vN$l^8z7bN*<|A6b7Tqg8HWM7IFdEII-;%h z+^><`#c*%^5D=4)a>sX0(M)zvRxJ^!UEXyXfJLPD5zyNFK=xF(yJ%FnwnQ%)% zA?F;}!~EGQ%QiCQfbV?!lX08Y9;%6F&;*5XZ_o2*9uvO=MqEdQ2KxH=F!Ni+{=B_f z`+$N-ZEC3+r6*0d!ERmGsbA*CG}dU4Q$#mb=P6o`v>;PbTl5e+7R`qOWeX?%a*>7z z!+!!;KJP3GBlY}j*|E0PLBFfi^R=_3r3x3|tgF@UN}?&d;&;f_BwXyTIgFKLM|L!r zWbdX$jlxN8c@Fgw9 zjXn1vug0oSU85K?!FZW9rwM~8HYHNP&#(}*bm~@b9khK4H*6N@@D?SkT=($$pj{0Z z!r4(e9cEH5;(PoU(Ul*vD*;-+0jgj5J_eO3r zPME@8|I%STiH0iJW)CaFfG<|f81uDv@S#G3y3vA@Yt1-l5_OIoTYkv6ik1SvB(;7D z)I$?%Lg_wckkIK3o^(_Q*bZE}fVq1xgs6n!=1kqDVFvmv48^^*_WX_g&rM1H7xjcLbZS4kj<9xM{v8hm5^(`4|B)A2?Q0%si~btW#wHh8w4_bjb%`M~@f+?{_Zj zTO?LY>$UT%{3jZEWmIGrK!-aF50E<+6I(m}Aw@;72{TcwheG)yT=oYikz2u{st6^r zYGOYyUm|iNa~M9CnCuNCq)xVDYcC~r3Zuou9w)Xl{o zSblIgF6uU?mlSJ(3;* zxs4}J)Uf$PJq}S9PVzUzZOC%wFD?UZnKGZaTA|RR-bfB)aykL7D8pfm3U0hGdQeHW zv23no;UwiPAaH`!EuZL5MBF&h^jq_-=V~(7a|P{|=}S9fI_NS_6uBSFJ*JZ^TiM;- z+Oin*EEJQ+YFH_I)IE~P*`=Tvcw9tJmz0v0H_aA!C5cbVIFzhY^Pp?o-mqrUhpY%j z_RtUtb#mR_y>tNLE_y)|x3VsUq{V);G)+vdtcH!Co~#Tl$^~_wtUQ%d0w1jsLm%yu ze+xwFJ~?^Hr>JjfvRDgT8a@exs;90!uz0_fD`=v7%I4cnSyMfc8?T-P1|tze@JNkQU29w>bj(IyzCd5{E?hQ#Y3nbL>(O z5ToO5H#M~XhTE$ApuWN9DBRZaZ*pn>4S7{{M_;SF8h%xyAG)g{I{66f%yeN$$9fxOwOvSi~>ZZ3T zY?S(Ddk9=`G%I%%J2*-8TGLG+WkdXAKj2tr2a5%+ax)t?^G+S&CF^HT?nD<18q*=_ z=fQi&QTLHI=p?GRkb_+dNy*^%(p)hNkEtq16ySADTa1*YoCKPthyx(gCX3W5qNrTI^| za+H=n1sH2h3SXA^Vr=7Q%_<`ZWXoA&y zxE@YMrfLYUThG6i(lVilaIT6#Ki36BsOu-Ik1;$)9dS5LV(KRsO9w;?PQ(5nO8JsC z8w-PPTp5U)M$Vs zrQ|^z8|Erw9IPIEqJRZW84w`2=VyOOx|7R! zQ2T%vy0laJt#8$Q@>5~%Ib_yPu( zMbygox~gTqYKm@NIp3eiJl>yAvDh92j|FR44wh3?O1Xfs2Ba3c1J*ylUWrWB!~tFK zDLJ?wU`{9_R)QT90cLOEs9K`)=cs?n*{=Q5a*!>2-`A3Ye4j%}b zwRX-;mFxF;{*;F|M*ECyrLftv3v7s;3E~>6cgLp`Cix%G({4$TJ!SCuVO@f|7UqVf z8sf@P1&5!qhu+So(BLiZ%sJ3F3Jgd7Q?3_PZ4tC*YkB3J~0G|ElJRLWEz{4I8yK!KG2xqnm?gy9TWqKex~&yF%&3KhRn)Utg>^$J!o+g%L^ zj|=#$m#xq4x!nxhm^PKDG|YV)yKJ&PIdP9vB&W_wlexUnPqTVV!lS(&|LmxA(ikn8 zvMn_R0g^>q;H@(yiOo2(tDtDM?5SBcl&|^JLb;+f%2K}+%kHfa9EM_udqmv@CCcIa zu~Zh-P2j*&mfFN**4!bd%J@#G4p0l!Z2zQOg(U6ZYI|U9AsogOJ2XdM{Se|oFY;~Z zN5mC*quGLLVH~RMx;+|nqxp;pKxErO;w?Ei0S4I1L^m+T)lPndKGlo*Mwa@C6x|li zstby;p;vyygdx?B1wSZ*n*9Z35wQ|Ok>9nZ77%8`wj}r`$Cm91dl9c}l3Y{lBGg9` zMKoj$(?3=dxjWxC&H)Qby{pd!sZOXF(-fNcblY_qgs*Bn4QqoR z4CkiEfbn8O1U2Dc3eL^H4(~kBe>#wVD}b=y`ZhkvX#TVUpcVMq4H1aD3dMCYGDc$Y zS#xsRgUOAPZ6osWUH@X7KAe!{)9+n;NJ);XyraOhp5{flM`=)5FfWTcyw%xL2z8Cy z7@QCKhpvd7Y--IELl^chN{9Gl7;d?dW|QdG>j!>3dp8yT^HGxz;`_0KXYwbz90bsx z>VJy93BVQ3Yc~F&f1-{3EsH6FrXkimpGDXTMk#`B9X(Ux@WZMOKApK<{ej%>yU z4S2vfywTs@e+v&W7^O{NW<~Z7M35JX67cH_az7P@c;tLfntdEkN-PwnrOF$}(wgug zrz(PYOqR}u2`d}+j$j8Bupb_Bn+t(-P0mMEhh)Fsb7EFc%DLhhKGgLEq9_P8ww2BT z3O@-ctXe|7;;S06r`LaZlLwkB3@~PyCmKX+i64D7_hfTQkE|j5(kC%(nwL|^_g0)9 zc6`eshL3k#UsO0AH=efaz6cEI_%(O9Xf0S*;sKMNEBDj-I*8^fZ0|~Byb}vxy8;{a zRD;;-a}^IkP(Hw14<2pCQaL24zJ@4qw6213zJO@?gx-WQjtgeq7|4Huc6Nil`p&Q! z^aODQ!@t*gqj2wn7(3@-V{e`_=Y@aisNcZ#$us=bKzAbVGxtzQ$NX&Z#_?7gu47cH zCC^Qy_+y8enFa(qI2SPM=fMI#J~$zcaa}v!>g(uiety)cTW5;a(KM?T_!N?{L-_kA zr7uvSFld$E!iO#+FoCbFoW_bnIt`?IPle<#yvuCJO>G@i(M{iaCFgli@mzE{bg2>M zm^HqWYXeckKTP+3Fslr6M~jNWr%KLV%h#c&8H6P88gh>&{RTztx(WwK@x2-8IRz@= zT6{s*WPv|rGp>8fnx(-_K#!NQ;3{Y-|RW!ZpWLX};&V88JfA9y5!_^N( zJ2$2$gy)s<%;wc|BW)a-Efbw8A)A8tS03QtEl=iioieEX3Z>zrFBZ!7ME(($eCdW; zFuTG3%7#3a^qUj)_0voLlWimW1@#J25RRA0IppUGLK+(CYrQPoO{;Rar;fim>r&*rOi)aJ zJ#rD~gc5ZW&58}`qQ*H|K**Pa@WQEVn^1+d2U&$qa}nbx%7+DzQdn}g!|t{V)JRTQ zeUMVNp=yv4I)%VXkP=b_#UmAs)2$C$f&i)B?o6A#4WGacO=pP=^X?mOnzL z(xG1ztrZvV>PrH%HNSAop8!9}H68!@PBIP%qM9RRBKl+OW>h_LHVLxT7phOXL>foQ z-@P0_Gl7McmU-;zVo z2Xep5gkcJ46b{U;1WGCIPJw)uvH#qp!ePkKqq*;_&}rbaG@c}!?CV-Uv}1GTff~#6 zjlItuK{K*6wb1mySqsoPXK%}}Zro`powb6&M1T7ZVL@l6I~1q&3VK0dcI0v9$zz=$ zx#ecFS;{g_9NuFpXBsd)c3~LyQ>3qz2B$C6`DJ0~06}ggOIt>Pabn)UfJX3sg;s24 zB_%plRiI7)6U|tT6ArzR7n4%mIF(v>07_Bi>>@Iwxw~gthI6{WJ`LN&n#D$U&uQd1 zojpGZQ|-*z#YPj%wjdbAN*x_O=BKGrAsaU;iro6O)th`OHTd1+tJMVx>*R=o()t4g z#274DSXT&8)sw>$LI0YzY^pld+^_tzCRZpp_}D1%wyX*rr3~FVyC?RKax6h!-)q3U z=%o%FUXI0hoSEUP_kNM+ z&4z6Ppyl5$T0}K1QQi0=O>y^G>|V~^H_>HV|C$EWZ;!fDU0Kg5n)?+<{AKd^kT}?S zGbWzNid>Aj7c5slB!YQdzj(5lKeav&*&#G{kkPg;S0_Z8$x;Q-;K@T`t0|Ju3Q{Af zWLBUl=-1XsCRQqWCN@O}XuW8@f#T37%0HCLR>L95Q1>AB4zFa2e+PyDo7_nBnaYpGr4|TjaQw}ewX!6{QnO$6UeUaVg6_D>irjLru-j7=GVsn zY|QYqFa*rxaCHbr;!LSp%&>-7YUtN6Vc3N?A-g$L?AH49T;`Vv^w55y{w$7@j6|@Y zNl5djQKn956k9W}E>;HnoOUwh^RlF0tCinC^11FQd%xoG`uRL1^nE`p1d=oKj||_H zA;L@m6m5kp#c?zt-9#*uVgo`4U4x$h5CP{|YmlG~-5u4B6CP4n>!BDZjjDl;+eJh1 zQ~iqG&tw+F=qtO;gm(ASEVk0{Q#_iHaz-^u*lmqER_7-g#v+T@l{4|vN%>1UpfxnR zBL3DH;Sf%>TL5ZA%l818YEhe ziREaC0Y!u5+(#Cl77>MPVX6K10*D#`EAIFG22>~Wa~7x4wv|c!wPgt}_ZtTlsBKi| z$hCDtI#}E+8|ZT4?#lES90O3C>G^7^*7Z=(t@=Nyw1D%WoYrJv(Ao>2*YwQzVW04` z#r~M-w8TR;rhsZ|1*Bwmw-upCeco-jIFn5_E=W+R!n``wVPQ?y;^|A_bLT9LY-!Ei zLqAZIsOw2PcU_+?D!@;a0xJmmKCZ`;tO)B<)TS*qwqL=_c7dfj3GeCGp`@INdkVYR ziB=HSK)^q=31`)4w^K1dlz7*m`M#xad#Uu6bV7It30>UUD@Vo+Z65Icb%sSs%yZQD zD!OLKW}ZCsx2{_9AS6tMzkGLqyKXNWm-41DY~(g1EZ$6040oY>!*5VnC!8dXE3I1QRC^P_nmzYsowjotNn+ zJXD1n5d6>fg&?4A7wM%aNHKj0(xGH{N`KuoCP(=#nL5T)@1(nQM>}|u?xf;+I+bB$ zllkdmjZcO8xQV4|XK-1koMnMFEjL4pmdx~h#y!2?=%zD_uiUyks>=(U@yYXw_Jn(t zjbn4jNQWqZ?Z5zFX!?#dSI`^6!}TN=DSE-1(4gJ-i&?^AlWS=77@*xG{TJ8C)>O3; z%VG6zx!Y*(`R~B{#K3J|Foe&A@IIcGT`k*o{VWn~^fx(^vZiL=4PWO|K%@+s8*GTil;SD@o2&!*DiSBM)eBJ+UdGv5{H;-t2 zqJJK_+Y>VaNmdLlHCkt@pu_m%teqLw!oOLW|MJp(XaRvO*?Mv1oDc5Yb2p7$cx6sg z@Q(a92d7nC2kFU5&Hl4RV~n6Rgi+l5mc6sYCT@hE|M!MCeO865j43WEJYh ztP*;cRpk?C7Q!|g4stalMQxLZDj3BwZEC#9b;Had!9@y*I>u*RsmCL#yW^$ti(PN_ zT9^0A<~>auRaev$G`VN$8&&4ek1w%0zavVRlI1^Z+nJIjr<&AVupZ1q=L=SAt}%Gj z6{AMq2BTRb-uVR4xjg?*RNQ@^!B)|``+s9#QyxIw9Beibd1dTX9yNWL#U}vm60?vh z(o7bJ7IOw3Rv&4y(jrHAnq}9~YLilxBsk*s@+orYHb@|I&}O^H1&g&jnE z*$nKe$dcIJS=s`ElNdiwBG37FI=k`+Oa9S#@PJo$zV@_)YB)Th zv8?=7Sh=Gq{Sau@ir>N>acQ1EMx^ZeJqnaXGJFUMe~XTjXjW-^%_{Kg&PSHr^R=6vEudcf4EHgTWbVkdzpB~!vvK8sqNuXc zB$e4>Q)rI;sgo`@$)_iFKG+yts=5zbi#j&)iM9UHLh%nx@T!TQhSL|j?44CCDGLaM z^9LtdCp?4W*XaB7c-ViyeqfRQX7^bY`Ca%>kXMt38%)R_iD3#p7h1L{JMY~QBG)ug z0x|vmGRI!>=rXDVqg3b1-(Ad8j#B;clxxa5 z^o`kXkpF(PIx?8d+2I;RFc6T#WWjJbK#$u(FJE1xn@lsLbrz14I07>z8XZ@RTw1{s)GX=!N^0%4{rmj{_`&!{++h^p%%mdyWN{<-IAOZyEt)ap0M2?- zSf6_|}ApK-Rc4_8EeIUy=e{n~6=>G|TYp!E782s&2?*BU=~k z-$XPBof#@jdbNdnvD6$!uNk`fF{nEGBZ)oQo0AEgRzV&OOx@Z+zS9jpUQ*%4!s@9} zyr;4q@BVsEMvWapyYX7|nT=v?RZ|%@@yd=7Vg~H&(!w~qLO)$vcOUUuAP9P26q$tG zg&)Bb9}PcQM1B`XEL+bO8`6N_XF=WRa9V)4Kr>h0`%!p-qf&qd&5!gT1ocykF zP&e2J-Kr1j%`6PLxPohW0Zj$@xS`23`^s=LUd04K{{`jCF0Hvpi5+T{+_9)a%;>~G zat#|NjM%xu=F`#=4Aeyppl|?@r9Ah(a%fgXki~VPs?zjwi^0lea&D6seZ8y5a*C(f z>~*%H^=DaCmhV#GC-1-xPe;F!DpPFlcWUR0jq;r2-w#P2{CZ_+c=p2Xn}}D)H-~wf zq-n$T;JH;Q@4|)`#BQRK3lX*&1kqtiN3ML%1<%qI747|JqPl@`GmWip%(m z&o={7zLak$c{4XdfAfcfugh~UzXERH{`B zwcAlKf7wGS*kex7heKz#ZAJ2iJ#CHcV6KlLh-^`gi-}O7^bz!*64w%4aFOD-kOZ#j zxN=LW1`b@p*9XHd%E3}|8d^qOXYZYmI$Nr#@IeJdkvJZ=Zw#OGS*%Nq*@FoT>qfc- zKV=KTctMDdDsicvgnNgUFpJ-TTq2QdJJH0v@n@6@oF{*QHcdqR07EDq8QJ;qUtu#F z4g`chxgmfc*?1Q!`7@RfP~DJ3|60bZCW{_y&j@KPM&$V6*SDEuoJ|gqrRUgezr~8YMq2;q4=A3q3z^fj~Jf-9gneTuskK(XVI3x`)Q7oP_6(k z@b!KU2jb>UYz7@ob&{Bf(nl(#7#2c-qoa?w2V3jvM~*pxPY3!0G{EDmaMwaP2k)20 z=)H&!gDi93vG!{pQ#)^(oV5LA!)?F`Yw+8uET&8A)L2^3U6QU_w&PgZ9LFmSkZQs0 zOeK3rGQoYq2*XR>zF9$u`&osMp1p3Ipn0yxJ3wQi?X*1J>7m7-HHJF9!qL)Mpc|&$ z7L$}efvht}w8-!YbeeEnm^N+Rjpc8$Ds1W2RK|uW)=MZQHPptP6pJ_ztxM!gH!;I6 zP8HVZdhRAVEGop!U_)+o;6-yf+_msz0_6d9rB(l@i}Ma^Vrly@E}Z}gH6er!3P@2v zN~i{;DIf^Ppny`8P!&Pxgh)LE1zdVl550-fLhnUE6jWL$fl#b8D~I}GKF)bxzWryO z=QsE4%r#rCo!ObE)Yb&E($qv!|x zDha<(&^i+vT#veJmR&q79*^~yB#juo>RXgn@@z|K{;Jbi4hFX#Q>LCgF6_(x%wfhk zk@%yq!17gWBxhe6m zu+h~!>qp=9w3k}GahAs}rRv9*u5Sg8%whp`|`{O91b+Xk2PqUz`;_ z{O5Xaw~9Va*A}uE(|FxCq)hLOt-(8lLZGnQaw0v4KLr+6g0%~&rVc^G)E2%vkGz3$ zqdlEhHb^-N8UBsJ8R`nLjul05?>-kiurYfpcyFA_ZvW(O;gxU6f@N-kBPx9KmIzKn zajA`8)?A3Dnc4-1mPx!f*)@@iy*JqL>5J1rOwi&jeKngI%ttrH@fLSvP!4N~ujyc> zX_ZUkS~I@JD!4%N&7wWm>Z+P_m+&6zsz~Ral=oM42d;t@S&W$gB+4MLC__ZYa=Bwo zp~CwO*&>hIVjH-kl{7`zJ9cSnO<3C^PFpoWr!HKyDg4(9)pPjZ$Uf=6qm}dA&#Fd4 zeOecPC^8Hg<+Vael8vi`zE||&qgMqs!Pgz38$yI~74aQ{?N|uaDAHdnjk|`um$g!B zx<^kY#A=hH$aL3wT>ztr2x%bRG-*ykCOL>v0zaWlhqNK)e#!=?h?c2ch|8D<_J;TE z3zmF(9=FYMPvY|`odM9`^2DNb$RwAyu;jLxCi9P-2vkfr7lMsoknJTz z(!>5~xbmUz=a0|u`xDtb>MNL^fUkS9g(g8`Nr^9Vd!(QkO&hgD>#9^=kwNeW4o zJBjR*8a8uHdQ=!_SkJ~N+W65X)I)CT0S=}QN~{d~L)s25Iy&uxw}u3M8oTAsJ0i3<%b`NjKz{dl*?&f=?IVXMDxx4mxK8X3dy2!@-Viy305jZfVXi{t`fP%%3Ey^{&+ z4`#2$!gJE-&*9HwlwuuO4OvK??5BHK^b?pJQ@WzN3`$_g6aAAXSz|ERsACZUvXT5+ zLY>M1sTR2qN42p2NL>i^eSBam3OWmKZWf(8qq8d|vR8^~>;1;<;53>h)hs?|b7TVL zw(eo#))lzNOBO8!MlO8tWW>l;xjoVD6vdjhnR#l^)$Mz!g>Qna>eLMFp$|M(ZpOc zAsbMp_1c+*aCB*15lVYPc-SlERsZIX$j4|IBE#6A=FFF6urvwx3%@$uL(LYOe)73~ zcTgLW9#rl9!91-!?OxOixIk2AuHu&uJsQ<+dZI(ly)P~gq)TQZXDV%*Ms`d(tqotM zXQIx_=ls%9YMc%#(B$n>V^IB)$6%RV}*e`RvASI7WC~JsTsFsEfok% zX`nKs!W_R`eTb$~yzw%9nA+@O)s;jUKeF0x*rE z*>ho0Rbh`Y_Hq69EScklULzX2BN{4R*{75m*XRYZe4zSmTzG8KvfOlPfiU%Fr%}wc zsXxt>GKUrN=s#aWY6-e{b_*$O!uW8lb!HzUCzOQWZnKZiijauaS1KOzGo%o|b!LC)Hv972QWY&#Nd@A=Mk0UM>{h_>`A4c`epgx~nk0q)y2x zBQMB~cswB^l^fp_{YjOz&!w3-uXIOTe4gPiC3A7vIe&lz_X~XJJ(+Cdur!piQ)ih1 zf33Qgn{PO{>Qo$mL0x`MTVQoQK3;dWI3Bw8I9~UbWaFlliBVC|%hD|fgLX>BCJe!}w(s^r%oe+NQE@P)p^_U@w!WdYQiIGCOi?j!1WkP9lr3@Frj0F8pMN#F zElyv!x(a0DlQi$cKegXF#sAi`$$O`l^HZ-jWHd$KW1yDCo|T3G2C9AQ652xe#r#I+ zh2ySIuXr@S$?F?^cr}MN?#SMy7pp69|{Fqdj#JU42>&~=Jnk{sp1B8Xl!{Ze?FLsAcQ+PFDF)`z#2 ziWrT<`&%mB&$G>LZ!xIml9ChA9tY}SllBW3&%kGpXUj+6PM^;{Z>*?)OA)~|dw{N183#zD_F z$mov)2B)t~PMq^J6|jh_x_h@(wBt2X!jin>z|0hpXq@>B#guKe`0%XSYX$$}87rjQqiMlh|HVe~LVXj%rk)9= z(A7_R@n$-)&?C0$v;jF_DQgdg=ttLr-kd(H$Gflf_gTo4KAf{$*XZqrf4AOaKH8n8 zesnkLES0i>35mkT9e>i+xd4)6ApVxwL?8U0TK;VhOD=|p+?li4M(l*~mlwWlj1%I% zbLC7%B=c?pxh&Cswvg@U%zVtiUr&uui8p=EdYC;bbU{+Ln-g0WGoKFT4M^t1KRo|8 z8yxu^V%!_iYOC~flTmVBj1-OtLL}5L?iQChijeKnlC6^NC217V{K~iz_!Ssx&tJ#m9cs)E1jRgi8;tZocfM@m~RcU+++rUM0BVHMWkA z<0C#-le#-#|1Z{5)QCEW96bSeFo6U)KCqPq1{O`jP=`XS>_^M^=g23RGarDzBd$oJ z{u@Mtj!x_!YCp{k(z(t-0pP3Lr9ooWls6KNA8uWiVnh>Z%E2!%JtHNei4X5J^G zQ2+fSLPw{5h-WdQL0Wbk;0Lla>d-9vA&}SN0OSD?b1=|l5(#+!L6b<%LNqBK2V?)I zNIoI#GA+}5iWz)`;{iFQWPw1314$Qn=L#lFSpX_HaCXWD2*rVF)0#l}zIR(0gw4P} z(lioK^VoL)Trvv8&YT9qd}!vYFenWiok0RKw`dY4MHP??+&3jaHwql} z@07=W*fGt2+O?nN6QDsfsEuL()P)|Hj3AWA0itJNs6%79L*+`sY4FZHL2!Zs18ZiH z07Dc_`ZjwCb?9sEP`TQeeMlFySb%}x91`G7pp{X~76g~)WC5NBG*_>P2~>H=Por>D zB!EcySFWI<0qOLAU6TSX8l^ms1f((#WNzC11S$RBOCXkWkjV~G=FtG`5zWOv=4HCH4Ee&F+Fwk!i2{5*UiHlf3rVA7s(xUbJ z`{DnsYo{ChF|0|;$XP-HL%m?b(pf;f4@AB@2Fkx@;Z&wmrt8}O&~@$m-8cUMZ39{l diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4b4429..6c9a224 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 8e25e6c..4f906e0 100755 --- a/gradlew +++ b/gradlew @@ -82,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -125,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -154,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 24467a1..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/settings.gradle.kts b/settings.gradle.kts index ef45f5a..d9daddd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,3 +35,5 @@ include("disco-java-agent-example") include("disco-java-agent-example-test") include("disco-java-agent-example-injector-test") include("disco-java-agent-instrumentation-preprocess") + +include("disco-toolkit-bom")