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