diff --git a/async/src/main/java/com/ea/async/instrumentation/Agent.java b/async/src/main/java/com/ea/async/instrumentation/Agent.java index 5d8a823..8bdeeb1 100644 --- a/async/src/main/java/com/ea/async/instrumentation/Agent.java +++ b/async/src/main/java/com/ea/async/instrumentation/Agent.java @@ -1,76 +1,56 @@ -/* - Copyright (C) 2015 Electronic Arts Inc. All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - 3. Neither the name of Electronic Arts, Inc. ("EA") nor the names of - its contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY ELECTRONIC ARTS AND ITS CONTRIBUTORS "AS IS" AND ANY - EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL ELECTRONIC ARTS OR ITS CONTRIBUTORS BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - package com.ea.async.instrumentation; import java.lang.instrument.Instrumentation; /** - * Class called when a java agent is attached to the jvm in runtime. + * Class called when a java agent is attached to the JVM in runtime. */ -public class Agent -{ +public class Agent { + /* * From https://docs.oracle.com/javase/8/docs/api/index.html?java/lang/instrument/Instrumentation.html - * + * * Agent-Class - * - * If an implementation supports a mechanism to start agents sometime - * after the VM has started then this attribute specifies the agent class. - * That is, the class containing the agentmain method. This attribute is - * required, if it is not present the agent will not be started. Note: this - * is a class name, not a file name or path. + * + * If an implementation supports a mechanism to start agents sometime after the VM has started, + * then this attribute specifies the agent class. That is, the class containing the agentmain method. + * This attribute is required; if it is not present, the agent will not be started. + * Note: this is a class name, not a file name or path. */ - public static void agentmain(String agentArgs, Instrumentation inst) - { + public static void agentmain(String agentArgs, Instrumentation inst) { Transformer transformer = new Transformer(); inst.addTransformer(transformer, true); + + // Loop through all loaded classes and retransform the ones that need instrumentation f1: - for (Class clazz : inst.getAllLoadedClasses()) - { + for (Class clazz : inst.getAllLoadedClasses()) { if (inst.isModifiableClass(clazz) && !clazz.getName().startsWith("java.") && !clazz.getName().startsWith("javax.") - && !clazz.getName().startsWith("sun.")) - { - try - { - if (transformer.needsInstrumentation(clazz)) - { + && !clazz.getName().startsWith("sun.")) { + try { + // Avoid retransformation if the class is already instrumented + if (transformer.needsInstrumentation(clazz) && !alreadyInstrumented(clazz)) { inst.retransformClasses(clazz); + markAsInstrumented(clazz); } - } - catch (Exception | Error e) - { + } catch (Exception | Error e) { e.printStackTrace(); } } } + + // Set a system property to indicate that EA async is running System.setProperty(Transformer.EA_ASYNC_RUNNING, "true"); } + + // Helper method to check if a class has already been instrumented by this agent + private static boolean alreadyInstrumented(Class clazz) { + return System.getProperty("instrumented." + clazz.getName()) != null; + } + + // Helper method to mark a class as instrumented + private static void markAsInstrumented(Class clazz) { + System.setProperty("instrumented." + clazz.getName(), "true"); + } } diff --git a/async/src/main/java/com/ea/async/instrumentation/Transformer.java b/async/src/main/java/com/ea/async/instrumentation/Transformer.java index b4e3703..53d3341 100644 --- a/async/src/main/java/com/ea/async/instrumentation/Transformer.java +++ b/async/src/main/java/com/ea/async/instrumentation/Transformer.java @@ -84,8 +84,7 @@ * * @author Daniel Sperry */ -public class Transformer implements ClassFileTransformer -{ +public class Transformer implements ClassFileTransformer { /** * Name of the property that will be set by the * Agent to flag that the instrumentation is already running. @@ -121,41 +120,35 @@ public class Transformer implements ClassFileTransformer public static final String JOIN_METHOD_NAME = "join"; public static final String JOIN_METHOD_DESC = "()Ljava/lang/Object;"; - public static final String LAMBDA_DESC = "(Ljava/lang/invoke/MethodHandles$Lookup;" - + "Ljava/lang/String;" - + "Ljava/lang/invoke/MethodType;" - + "Ljava/lang/invoke/MethodType;" - + "Ljava/lang/invoke/MethodHandle;" - + "Ljava/lang/invoke/MethodType;" - + ")Ljava/lang/invoke/CallSite;"; + public static final String LAMBDA_DESC = "(Ljava/lang/invoke/MethodHandles$Lookup;" + + "Ljava/lang/String;" + + "Ljava/lang/invoke/MethodType;" + + "Ljava/lang/invoke/MethodType;" + + "Ljava/lang/invoke/MethodHandle;" + + "Ljava/lang/invoke/MethodType;" + + ")Ljava/lang/invoke/CallSite;"; public static final Handle METAFACTORY_HANDLE = new Handle(Opcodes.H_INVOKESTATIC, - "java/lang/invoke/LambdaMetafactory", - "metafactory", - LAMBDA_DESC); + "java/lang/invoke/LambdaMetafactory", + "metafactory", + LAMBDA_DESC); - public static WeakHashMap futureMap = new WeakHashMap<>(); - public static WeakHashMap> classLoaderMap = new WeakHashMap<>(); + public static WeakHashMap < URL, Boolean > futureMap = new WeakHashMap < > (); + public static WeakHashMap < ClassLoader, Set < URL >> classLoaderMap = new WeakHashMap < > (); - private Consumer errorListener; + private Consumer < String > errorListener; @Override - public byte[] transform(final ClassLoader loader, final String className, final Class classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) throws IllegalClassFormatException - { - try - { - if (className != null && className.startsWith("java")) - { + public byte[] transform(final ClassLoader loader, final String className, final Class << ? > classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) throws IllegalClassFormatException { + try { + if (className != null && className.startsWith("java")) { return null; } ClassReader cr = new ClassReader(classfileBuffer); - if (needsInstrumentation(cr)) - { + if (needsInstrumentation(cr)) { return transform(loader, cr); } return null; - } - catch (Exception | Error e) - { + } catch (Exception | Error e) { // Avoid using slf4j or any dependency here. // this is supposed to be a critical error. // it should be ok to write directly to the syserr. @@ -166,8 +159,7 @@ public byte[] transform(final ClassLoader loader, final String className, final } } - static class SwitchEntry - { + static class SwitchEntry { final Label resumeLabel; final Label futureIsDoneLabel; final int key; @@ -178,8 +170,7 @@ static class SwitchEntry public int[] argumentToLocal; public int[] localToiArgument; - public SwitchEntry(final int key, final FrameAnalyzer.ExtendedFrame frame, final int index) - { + public SwitchEntry(final int key, final FrameAnalyzer.ExtendedFrame frame, final int index) { this.key = key; this.frame = frame; this.index = index; @@ -188,8 +179,7 @@ public SwitchEntry(final int key, final FrameAnalyzer.ExtendedFrame frame, final } } - static class Argument - { + static class Argument { BasicValue value; String name; int iArgumentLocal; @@ -197,18 +187,13 @@ static class Argument int tmpLocalMapping = -1; } - public byte[] instrument(final ClassLoader classLoader, InputStream inputStream) - { - try - { + public byte[] instrument(final ClassLoader classLoader, InputStream inputStream) { + try { ClassReader cr = new ClassReader(inputStream); - if (needsInstrumentation(cr)) - { + if (needsInstrumentation(cr)) { return transform(classLoader, cr); } - } - catch (Exception e) - { + } catch (Exception e) { throw new RuntimeException(e); } return null; @@ -221,88 +206,70 @@ public byte[] instrument(final ClassLoader classLoader, InputStream inputStream) * @param cr the class reader for this class * @return null or the new class bytes */ - public byte[] transform(final ClassLoader classLoader, ClassReader cr) throws AnalyzerException - { + public byte[] transform(final ClassLoader classLoader, ClassReader cr) throws AnalyzerException { ClassNode classNode = new ClassNode(); // using EXPAND_FRAMES because F_SAME causes problems when inserting new frames cr.accept(classNode, ClassReader.EXPAND_FRAMES); int countInstrumented = 0; - Map nameUseCount = new HashMap<>(); + Map < String, Integer > nameUseCount = new HashMap < > (); - for (MethodNode original : (List) new ArrayList(classNode.methods)) - { + for (MethodNode original: (List < MethodNode > ) new ArrayList(classNode.methods)) { Integer countOriginalUses = nameUseCount.get(original.name); nameUseCount.put(original.name, countOriginalUses == null ? 1 : countOriginalUses + 1); boolean hasAwaitCall = false; boolean hasAwaitInitCall = false; - for (Iterator it = original.instructions.iterator(); it.hasNext(); ) - { + for (Iterator it = original.instructions.iterator(); it.hasNext();) { Object o = it.next(); - if (o instanceof MethodInsnNode) - { - if (!hasAwaitCall) - { + if (o instanceof MethodInsnNode) { + if (!hasAwaitCall) { hasAwaitCall = isAwaitCall((MethodInsnNode) o); } - if (!hasAwaitInitCall) - { + if (!hasAwaitInitCall) { hasAwaitInitCall = isAwaitInitCall((MethodInsnNode) o); } } } - if (!hasAwaitCall && !hasAwaitInitCall) - { + if (!hasAwaitCall && !hasAwaitInitCall) { continue; } countInstrumented++; boolean nonCompFutReturn = !original.desc.endsWith(COMPLETABLE_FUTURE_RET) && !original.desc.endsWith(COMPLETION_STAGE_RET); - if (original.desc.endsWith(COMPLETABLE_FUTURE_RET) - || original.desc.endsWith(COMPLETION_STAGE_RET) - || returnsCompletionStageSubClass(classLoader, original) - ) - { + if (original.desc.endsWith(COMPLETABLE_FUTURE_RET) || + original.desc.endsWith(COMPLETION_STAGE_RET) || + returnsCompletionStageSubClass(classLoader, original) + ) { // async method transformAsyncMethod(classNode, original, nameUseCount); - } - else - { + } else { // non async method // Removing calls to `Await.init()` // and advising on wrong uses of Await.await final MethodNode replacement = new MethodNode(original.access, - original.name, original.desc, original.signature, (String[]) original.exceptions.toArray(new String[original.exceptions.size()])); + original.name, original.desc, original.signature, (String[]) original.exceptions.toArray(new String[original.exceptions.size()])); - class MyMethodVisitor extends MethodVisitor - { - public MyMethodVisitor(final MethodNode mv) - { + class MyMethodVisitor extends MethodVisitor { + public MyMethodVisitor(final MethodNode mv) { super(Opcodes.ASM5, mv); } @Override - public void visitMethodInsn(final int opcode, final String owner, final String name, final String desc, final boolean itf) - { - if (isAwaitCall(opcode, owner, name, desc)) - { + public void visitMethodInsn(final int opcode, final String owner, final String name, final String desc, final boolean itf) { + if (isAwaitCall(opcode, owner, name, desc)) { // replaces invalid awaits with the join notifyError("Invalid use of await at %s.%s", cr.getClassName(), original.name); visitMethodInsn(INVOKEINTERFACE, COMPLETION_STAGE_NAME, "toCompletableFuture", "()Ljava/util/concurrent/CompletableFuture;", true); visitMethodInsn(INVOKEVIRTUAL, COMPLETABLE_FUTURE_NAME, JOIN_METHOD_NAME, JOIN_METHOD_DESC, false); - } - else if (isAwaitInitCall(opcode, owner, name, desc)) - { + } else if (isAwaitInitCall(opcode, owner, name, desc)) { // replaces all references to Await.init with NOP super.visitInsn(NOP); - } - else - { + } else { super.visitMethodInsn(opcode, owner, name, desc, itf); } } @@ -314,17 +281,14 @@ else if (isAwaitInitCall(opcode, owner, name, desc)) } } // no changes. - if (countInstrumented == 0) - { + if (countInstrumented == 0) { return null; } // avoiding using COMPUTE_FRAMES - final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS) - { + final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS) { @Override - protected String getCommonSuperClass(final String type1, final String type2) - { + protected String getCommonSuperClass(final String type1, final String type2) { // this is only called if COMPUTE_FRAMES is enabled // implementing this properly would require loading information @@ -343,79 +307,63 @@ protected String getCommonSuperClass(final String type1, final String type2) return bytes; } - private boolean returnsCompletionStageSubClass(ClassLoader classLoader, final MethodNode original) - { + private boolean returnsCompletionStageSubClass(ClassLoader classLoader, final MethodNode original) { final Type retType = Type.getReturnType(original.desc); - if (retType.getSort() != Type.OBJECT) - { + if (retType.getSort() != Type.OBJECT) { return false; } final String retTypeName = retType.getInternalName(); return isCompletionStage(classLoader, retTypeName); } - private boolean isCompletionStage(final ClassLoader classLoader, final String internalName) - { - if (COMPLETION_STAGE_NAME.equals(internalName)) - { + private boolean isCompletionStage(final ClassLoader classLoader, final String internalName) { + if (COMPLETION_STAGE_NAME.equals(internalName)) { return true; } - if (COMPLETABLE_FUTURE_NAME.equals(internalName)) - { + if (COMPLETABLE_FUTURE_NAME.equals(internalName)) { return true; } URL resource = classLoader.getResource(internalName + ".class"); Boolean aBoolean; - synchronized (futureMap) - { + synchronized(futureMap) { aBoolean = futureMap.get(resource); } - if (aBoolean != null) - { + if (aBoolean != null) { return aBoolean; } aBoolean = isCompletionStage(classLoader, classLoader.getResourceAsStream(internalName + ".class")); - synchronized (classLoaderMap) - { - Set urls = classLoaderMap.get(classLoader); - if (urls == null) - { + synchronized(classLoaderMap) { + Set < URL > urls = classLoaderMap.get(classLoader); + if (urls == null) { // class loader hold the references to the urls - classLoaderMap.put(classLoader, urls = new HashSet<>()); + classLoaderMap.put(classLoader, urls = new HashSet < > ()); } urls.add(resource); } - synchronized (futureMap) - { + synchronized(futureMap) { futureMap.put(resource, aBoolean); } return aBoolean; } - private boolean isCompletionStage(ClassLoader classLoader, InputStream resource) - { - if (resource == null) - { + private boolean isCompletionStage(ClassLoader classLoader, InputStream resource) { + if (resource == null) { return false; } - try - { + try { return isCompletionStage(classLoader, new ClassReader(resource).getSuperName()); - } - catch (IOException e) - { + } catch (IOException e) { return false; } } - private void transformAsyncMethod(final ClassNode classNode, final MethodNode original, final Map nameUseCount) throws AnalyzerException - { - final List switchEntries = new ArrayList<>(); + private void transformAsyncMethod(final ClassNode classNode, final MethodNode original, final Map < String, Integer > nameUseCount) throws AnalyzerException { + final List < SwitchEntry > switchEntries = new ArrayList < > (); SwitchEntry entryPoint; - List arguments = new ArrayList<>(); - final List