1 /*
   2  * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.jfr.event.io;
  27 
  28 import java.util.Arrays;
  29 import java.util.Set;
  30 import java.util.HashSet;
  31 import java.io.File;
  32 import java.security.ProtectionDomain;
  33 import java.lang.instrument.ClassFileTransformer;
  34 import java.lang.instrument.Instrumentation;
  35 import java.lang.instrument.IllegalClassFormatException;
  36 
  37 import jdk.internal.org.objectweb.asm.ClassReader;
  38 import jdk.internal.org.objectweb.asm.ClassVisitor;
  39 import jdk.internal.org.objectweb.asm.MethodVisitor;
  40 import jdk.internal.org.objectweb.asm.ClassWriter;
  41 import jdk.internal.org.objectweb.asm.Opcodes;
  42 import jdk.internal.org.objectweb.asm.Type;
  43 import jdk.testlibrary.OutputAnalyzer;
  44 import jdk.testlibrary.ProcessTools;
  45 import jdk.testlibrary.jfr.IOEvent;
  46 import jdk.testlibrary.jfr.InstrumentationCallback;
  47 
  48 /*
  49  * @test
  50  * @summary Test that will instrument the same classes that JFR will also instrument.
  51  * @key jfr
  52  *
  53  * @library /lib/testlibrary
  54  * @modules java.base/jdk.internal.org.objectweb.asm
  55  *          java.instrument
  56  *          jdk.jartool/sun.tools.jar
  57  *          jdk.jfr
  58  *
  59  * @run main/othervm jdk.jfr.event.io.TestInstrumentation
  60  */
  61 
  62 // Test that will instrument the same classes that JFR will also instrument.
  63 //
  64 // The methods that will be instrumented, for example java.io.RandomAccessFile.write,
  65 // will add the following code at the start of the method:
  66 // InstrumentationCallback.callback("<classname>::<methodname>");
  67 //
  68 // The class InstrumentationCallback will log all keys added by the callback() function.
  69 //
  70 // With this instrumentation in place, we will run some existing jfr.io tests
  71 // to verify that our instrumentation has not broken the JFR instrumentation.
  72 //
  73 // After the tests have been run, we verify that the callback() function have been
  74 // called from all instrumented classes and methods. This will verify that JFR has not
  75 // broken our instrumentation.
  76 //
  77 // To use instrumentation, the test must be run in a new java process with
  78 // the -javaagent option.
  79 // We must also create two jars:
  80 // TestInstrumentation.jar: The javaagent for the instrumentation.
  81 // InstrumentationCallback.jar: This is a separate jar with the instrumentation
  82 // callback() function. It is in a separate jar because it must be added to
  83 // the bootclasspath to be called from java.io classes.
  84 //
  85 // The test contains 3 parts:
  86 // Setup part that will create jars and launch the new test instance.
  87 // Agent part that contains the instrumentation code.
  88 // The actual test part is in the TestMain class.
  89 //
  90 public class TestInstrumentation implements ClassFileTransformer {
  91 
  92     private static Instrumentation instrumentation = null;
  93     private static TestInstrumentation testTransformer = null;
  94 
  95     // All methods that will be instrumented.
  96     private static final String[] instrMethodKeys = {
  97         "java/io/RandomAccessFile::seek::(J)V",
  98         "java/io/RandomAccessFile::read::()I",
  99         "java/io/RandomAccessFile::read::([B)I",
 100         "java/io/RandomAccessFile::write::([B)V",
 101         "java/io/RandomAccessFile::write::(I)V",
 102         "java/io/RandomAccessFile::close::()V",
 103         "java/io/FileInputStream::read::([BII)I",
 104         "java/io/FileInputStream::read::([B)I",
 105         "java/io/FileInputStream::read::()I",
 106         "java/io/FileOutputStream::write::(I)V",
 107         "java/io/FileOutputStream::write::([B)V",
 108         "java/io/FileOutputStream::write::([BII)V",
 109         "java/net/SocketInputStream::read::()I",
 110         "java/net/SocketInputStream::read::([B)I",
 111         "java/net/SocketInputStream::read::([BII)I",
 112         "java/net/SocketInputStream::close::()V",
 113         "java/net/SocketOutputStream::write::(I)V",
 114         "java/net/SocketOutputStream::write::([B)V",
 115         "java/net/SocketOutputStream::write::([BII)V",
 116         "java/net/SocketOutputStream::close::()V",
 117         "java/nio/channels/FileChannel::read::([Ljava/nio/ByteBuffer;)J",
 118         "java/nio/channels/FileChannel::write::([Ljava/nio/ByteBuffer;)J",
 119         "java/nio/channels/SocketChannel::open::()Ljava/nio/channels/SocketChannel;",
 120         "java/nio/channels/SocketChannel::open::(Ljava/net/SocketAddress;)Ljava/nio/channels/SocketChannel;",
 121         "java/nio/channels/SocketChannel::read::([Ljava/nio/ByteBuffer;)J",
 122         "java/nio/channels/SocketChannel::write::([Ljava/nio/ByteBuffer;)J",
 123         "sun/nio/ch/FileChannelImpl::read::(Ljava/nio/ByteBuffer;)I",
 124         "sun/nio/ch/FileChannelImpl::write::(Ljava/nio/ByteBuffer;)I",
 125     };
 126 
 127     private static String getInstrMethodKey(String className, String methodName, String signature) {
 128         // This key is used to identify a class and method. It is sent to callback(key)
 129         return className + "::" + methodName + "::" + signature;
 130     }
 131 
 132     private static String getClassFromMethodKey(String methodKey) {
 133         return methodKey.split("::")[0];
 134     }
 135 
 136     // Set of all classes targeted for instrumentation.
 137     private static Set<String> instrClassesTarget = null;
 138 
 139     // Set of all classes where instrumentation has been completed.
 140     private static Set<String> instrClassesDone = null;
 141 
 142     static {
 143         // Split class names from InstrMethodKeys.
 144         instrClassesTarget = new HashSet<String>();
 145         instrClassesDone = new HashSet<String>();
 146         for (String s : instrMethodKeys) {
 147             String className = getClassFromMethodKey(s);
 148             instrClassesTarget.add(className);
 149         }
 150     }
 151 
 152     private static void log(String msg) {
 153         System.out.println("TestTransformation: " + msg);
 154     }
 155 
 156 
 157     ////////////////////////////////////////////////////////////////////
 158     // This is the actual test part.
 159     // A batch of jfr io tests will be run twice with a
 160     // retransfromClasses() in between. After each test batch we verify
 161     // that all callbacks have been called.
 162     ////////////////////////////////////////////////////////////////////
 163 
 164     public static class TestMain {
 165 
 166         private enum TransformStatus { Transformed, Retransformed, Removed }
 167 
 168         public static void main(String[] args) throws Throwable {
 169             runAllTests(TransformStatus.Transformed);
 170 
 171             // Retransform all classes and then repeat tests
 172             Set<Class<?>> classes = new HashSet<Class<?>>();
 173             for (String className : instrClassesTarget) {
 174                 Class<?> clazz = Class.forName(className.replaceAll("/", "."));
 175                 classes.add(clazz);
 176                 log("Will retransform " + clazz.getName());
 177             }
 178             instrumentation.retransformClasses(classes.toArray(new Class<?>[0]));
 179 
 180             // Clear all callback keys so we don't read keys from the previous test run.
 181             InstrumentationCallback.clear();
 182             runAllTests(TransformStatus.Retransformed);
 183 
 184             // Remove my test transformer and run tests again. Should not get any callbacks.
 185             instrumentation.removeTransformer(testTransformer);
 186             instrumentation.retransformClasses(classes.toArray(new Class<?>[0]));
 187             InstrumentationCallback.clear();
 188             runAllTests(TransformStatus.Removed);
 189         }
 190 
 191         // This is not all available jfr io tests, but a reasonable selection.
 192         public static void runAllTests(TransformStatus status) throws Throwable {
 193             log("runAllTests, TransformStatus: " + status);
 194             try {
 195                 String[] noArgs = new String[0];
 196                 TestRandomAccessFileEvents.main(noArgs);
 197                 TestSocketEvents.main(noArgs);
 198                 TestSocketChannelEvents.main(noArgs);
 199                 TestFileChannelEvents.main(noArgs);
 200                 TestFileStreamEvents.main(noArgs);
 201                 TestDisabledEvents.main(noArgs);
 202 
 203                 // Verify that all expected callbacks have been called.
 204                 Set<String> callbackKeys = InstrumentationCallback.getKeysCopy();
 205                 for (String key : instrMethodKeys) {
 206                     boolean gotCallback = callbackKeys.contains(key);
 207                     boolean expectsCallback = isClassInstrumented(status, key);
 208                     String msg = String.format("key:%s, expects:%b", key, expectsCallback);
 209                     if (gotCallback != expectsCallback) {
 210                         throw new Exception("Wrong callback() for " + msg);
 211                     } else {
 212                         log("Correct callback() for " + msg);
 213                     }
 214                 }
 215             } catch (Throwable t) {
 216                 log("Test failed in phase " + status);
 217                 t.printStackTrace();
 218                 throw t;
 219             }
 220         }
 221 
 222         private static boolean isClassInstrumented(TransformStatus status, String key) throws Throwable {
 223             switch (status) {
 224             case Retransformed:
 225                 return true;
 226             case Removed:
 227                 return false;
 228             case Transformed:
 229                 String className = getClassFromMethodKey(key);
 230                 return instrClassesDone.contains(className);
 231             }
 232             throw new Exception("Test error: Unknown TransformStatus: " + status);
 233         }
 234     }
 235 
 236 
 237     ////////////////////////////////////////////////////////////////////
 238     // This is the setup part. It will create needed jars and
 239     // launch a new java instance that will run the internal class TestMain.
 240     // This setup step is needed because we must use a javaagent jar to
 241     // transform classes.
 242     ////////////////////////////////////////////////////////////////////
 243 
 244     public static void main(String[] args) throws Throwable {
 245         buildJar("TestInstrumentation", true);
 246         buildJar("InstrumentationCallback", false);
 247         launchTest();
 248     }
 249 
 250     private static void buildJar(String jarName, boolean withManifest) throws Throwable {
 251         final String slash = File.separator;
 252         final String packageName = "jdk/jfr/event/io".replace("/", slash);
 253         System.out.println("buildJar packageName: " + packageName);
 254 
 255         String testClasses = System.getProperty("test.classes", "?");
 256         String testSrc = System.getProperty("test.src", "?");
 257         String jarPath = testClasses + slash + jarName + ".jar";
 258         String manifestPath = testSrc + slash + jarName + ".mf";
 259         String className = packageName + slash + jarName + ".class";
 260 
 261         String[] args = null;
 262         if (withManifest) {
 263             args = new String[] {"-cfm", jarPath, manifestPath, "-C", testClasses, className};
 264         } else {
 265             args = new String[] {"-cf", jarPath, "-C", testClasses, className};
 266         }
 267 
 268         log("Running jar " + Arrays.toString(args));
 269         sun.tools.jar.Main jarTool = new sun.tools.jar.Main(System.out, System.err, "jar");
 270         if (!jarTool.run(args)) {
 271             throw new Exception("jar failed: args=" + Arrays.toString(args));
 272         }
 273     }
 274 
 275     // Launch the test instance. Will run the internal class TestMain.
 276     private static void launchTest() throws Throwable {
 277         final String slash = File.separator;
 278 
 279         // Need to add jdk/lib/tools.jar to classpath.
 280         String classpath =
 281             System.getProperty("test.class.path", "") + File.pathSeparator +
 282             System.getProperty("test.jdk", ".") + slash + "lib" + slash + "tools.jar";
 283         String testClassDir = System.getProperty("test.classes", "") + slash;
 284 
 285         String[] args = {
 286             "-Xbootclasspath/a:" + testClassDir + "InstrumentationCallback.jar",
 287             "--add-exports", "java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED",
 288             "-classpath", classpath,
 289             "-javaagent:" + testClassDir + "TestInstrumentation.jar",
 290             "jdk.jfr.event.io.TestInstrumentation$TestMain" };
 291         OutputAnalyzer output = ProcessTools.executeTestJvm(args);
 292         output.shouldHaveExitValue(0);
 293     }
 294 
 295 
 296     ////////////////////////////////////////////////////////////////////
 297     // This is the java agent part. Used to transform classes.
 298     //
 299     // Each transformed method will add this call:
 300     // InstrumentationCallback.callback("<classname>::<methodname>");
 301     ////////////////////////////////////////////////////////////////////
 302 
 303     public static void premain(String args, Instrumentation inst) throws Exception {
 304         instrumentation = inst;
 305         testTransformer = new TestInstrumentation();
 306         inst.addTransformer(testTransformer, true);
 307     }
 308 
 309     public byte[] transform(
 310             ClassLoader classLoader, String className, Class<?> classBeingRedefined,
 311             ProtectionDomain pd, byte[] bytes) throws IllegalClassFormatException {
 312         // Check if this class should be instrumented.
 313         if (!instrClassesTarget.contains(className)) {
 314             return null;
 315         }
 316 
 317         boolean isRedefinition = classBeingRedefined != null;
 318         log("instrument class(" + className + ") " + (isRedefinition ? "redef" : "load"));
 319 
 320         ClassReader reader = new ClassReader(bytes);
 321         ClassWriter writer = new ClassWriter(
 322                 reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
 323         CallbackClassVisitor classVisitor = new CallbackClassVisitor(writer);
 324         reader.accept(classVisitor, 0);
 325         instrClassesDone.add(className);
 326         return writer.toByteArray();
 327     }
 328 
 329     private static class CallbackClassVisitor extends ClassVisitor {
 330         private String className;
 331 
 332         public CallbackClassVisitor(ClassVisitor cv) {
 333             super(Opcodes.ASM5, cv);
 334         }
 335 
 336         @Override
 337         public void visit(
 338                 int version, int access, String name, String signature,
 339                 String superName, String[] interfaces) {
 340             cv.visit(version, access, name, signature, superName, interfaces);
 341             className = name;
 342         }
 343 
 344         @Override
 345         public MethodVisitor visitMethod(
 346                 int access, String methodName, String desc, String signature, String[] exceptions) {
 347             String methodKey = getInstrMethodKey(className, methodName, desc);
 348             boolean isInstrumentedMethod = Arrays.asList(instrMethodKeys).contains(methodKey);
 349             MethodVisitor mv = cv.visitMethod(access, methodName, desc, signature, exceptions);
 350             if (isInstrumentedMethod && mv != null) {
 351                 mv = new CallbackMethodVisitor(mv, methodKey);
 352                 log("instrumented " + methodKey);
 353             }
 354             return mv;
 355         }
 356     }
 357 
 358     public static class CallbackMethodVisitor extends MethodVisitor {
 359         private String logMessage;
 360 
 361         public CallbackMethodVisitor(MethodVisitor mv, String logMessage) {
 362             super(Opcodes.ASM5, mv);
 363             this.logMessage = logMessage;
 364         }
 365 
 366         @Override
 367         public void visitCode() {
 368             mv.visitCode();
 369             String methodDescr = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(String.class));
 370             String className = InstrumentationCallback.class.getName().replace('.', '/');
 371             mv.visitLdcInsn(logMessage);
 372             mv.visitMethodInsn(Opcodes.INVOKESTATIC, className, "callback", methodDescr);
 373         }
 374     }
 375 
 376 }