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