/* * Copyright 2012-2017 the original author or authors. * * 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. */ package org.glowroot.agent.weaving; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.security.CodeSource; import java.util.List; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.StandardSystemProperty; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.io.Files; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.commons.AdviceAdapter; import org.objectweb.asm.commons.JSRInlinerAdapter; import org.objectweb.asm.util.CheckClassAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.glowroot.agent.config.ConfigService; import org.glowroot.agent.impl.ThreadContextImpl; import org.glowroot.agent.impl.TimerImpl; import org.glowroot.agent.impl.TimerNameCache; import org.glowroot.agent.impl.TransactionRegistry; import org.glowroot.agent.plugin.api.TimerName; import org.glowroot.agent.plugin.api.config.ConfigListener; import org.glowroot.agent.plugin.api.weaving.Pointcut; import org.glowroot.agent.weaving.AnalyzedWorld.ParseContext; import static com.google.common.base.Preconditions.checkNotNull; import static org.objectweb.asm.Opcodes.ASM5; public class Weaver { private static final Logger logger = LoggerFactory.getLogger(Weaver.class); // useful for debugging java.lang.VerifyErrors private static final boolean VERIFY_WEAVING = Boolean.getBoolean("glowroot.weaving.verify"); private final Supplier<List<Advice>> advisors; private final ImmutableList<ShimType> shimTypes; private final ImmutableList<MixinType> mixinTypes; private final AnalyzedWorld analyzedWorld; private final TransactionRegistry transactionRegistry; private final TimerName timerName; private volatile boolean enabled; public Weaver(Supplier<List<Advice>> advisors, List<ShimType> shimTypes, List<MixinType> mixinTypes, AnalyzedWorld analyzedWorld, TransactionRegistry transactionRegistry, TimerNameCache timerNameCache, final ConfigService configService) { this.advisors = advisors; this.shimTypes = ImmutableList.copyOf(shimTypes); this.mixinTypes = ImmutableList.copyOf(mixinTypes); this.analyzedWorld = analyzedWorld; this.transactionRegistry = transactionRegistry; configService.addConfigListener(new ConfigListener() { @Override public void onChange() { enabled = configService.getAdvancedConfig().weavingTimer(); } }); this.timerName = timerNameCache.getTimerName(OnlyForTheTimerName.class); } byte /*@Nullable*/[] weave(byte[] classBytes, String className, @Nullable CodeSource codeSource, @Nullable ClassLoader loader) { TimerImpl weavingTimer = startWeavingTimer(); try { logger.trace("transform(): className={}", className); byte[] transformedBytes = weaveUnderTimer(classBytes, className, codeSource, loader); if (transformedBytes != null) { logger.debug("transform(): transformed {}", className); } return transformedBytes; } finally { if (weavingTimer != null) { weavingTimer.stop(); } } } private @Nullable TimerImpl startWeavingTimer() { if (!enabled) { return null; } ThreadContextImpl threadContext = transactionRegistry.getCurrentThreadContextHolder().get(); if (threadContext == null) { return null; } TimerImpl currentTimer = threadContext.getCurrentTimer(); if (currentTimer == null) { return null; } return currentTimer.startNestedTimer(timerName); } private byte/*@Nullable*/[] weaveUnderTimer(byte[] classBytes, String className, @Nullable CodeSource codeSource, @Nullable ClassLoader loader) { List<Advice> advisors = analyzedWorld.mergeInstrumentationAnnotations(this.advisors.get(), classBytes, loader, className); ThinClassVisitor accv = new ThinClassVisitor(); new ClassReader(classBytes).accept(accv, ClassReader.SKIP_FRAMES + ClassReader.SKIP_CODE); byte[] maybeProcessedBytes = null; if (className.equals("org/apache/felix/framework/BundleWiringImpl")) { ClassWriter cw = new ComputeFramesClassWriter(ClassWriter.COMPUTE_FRAMES, analyzedWorld, loader, codeSource, className); ClassVisitor cv = new FelixOsgiHackClassVisitor(cw); ClassReader cr = new ClassReader(classBytes); cr.accept(new JSRInlinerClassVisitor(cv), ClassReader.SKIP_FRAMES); maybeProcessedBytes = cw.toByteArray(); } else if (className.equals("org/jboss/system/server/ServerImpl")) { ClassWriter cw = new ComputeFramesClassWriter(ClassWriter.COMPUTE_FRAMES, analyzedWorld, loader, codeSource, className); ClassVisitor cv = new JBoss4HackClassVisitor(cw); ClassReader cr = new ClassReader(classBytes); cr.accept(new JSRInlinerClassVisitor(cv), ClassReader.SKIP_FRAMES); maybeProcessedBytes = cw.toByteArray(); } ClassAnalyzer classAnalyzer = new ClassAnalyzer(accv.getThinClass(), advisors, shimTypes, mixinTypes, loader, analyzedWorld, codeSource, classBytes); classAnalyzer.analyzeMethods(); if (!classAnalyzer.isWeavingRequired()) { analyzedWorld.add(classAnalyzer.getAnalyzedClass(), loader); return maybeProcessedBytes; } // from http://www.oracle.com/technetwork/java/javase/compatibility-417013.html: // // "Classfiles with version number 51 are exclusively verified using the type-checking // verifier, and thus the methods must have StackMapTable attributes when appropriate. // For classfiles with version 50, the Hotspot JVM would (and continues to) failover to // the type-inferencing verifier if the stackmaps in the file were missing or incorrect. // This failover behavior does not occur for classfiles with version 51 (the default // version for Java SE 7). // Any tool that modifies bytecode in a version 51 classfile must be sure to update the // stackmap information to be consistent with the bytecode in order to pass // verification." // ClassWriter cw = new ComputeFramesClassWriter(ClassWriter.COMPUTE_FRAMES, analyzedWorld, loader, codeSource, className); WeavingClassVisitor cv = new WeavingClassVisitor(cw, loader, classAnalyzer.getAnalyzedClass(), classAnalyzer.getMethodsThatOnlyNowFulfillAdvice(), classAnalyzer.getMatchedShimTypes(), classAnalyzer.getMatchedMixinTypes(), classAnalyzer.getMethodAdvisors(), analyzedWorld); ClassReader cr = new ClassReader(maybeProcessedBytes == null ? classBytes : maybeProcessedBytes); try { cr.accept(new JSRInlinerClassVisitor(cv), ClassReader.SKIP_FRAMES); } catch (RuntimeException e) { logger.error("unable to weave {}: {}", className, e.getMessage(), e); try { File tempFile = getTempFile(className, "glowroot-weaving-error-", ".class"); Files.write(classBytes, tempFile); logger.error("wrote bytecode to: {}", tempFile.getAbsolutePath()); } catch (IOException f) { logger.error(f.getMessage(), f); } return null; } byte[] transformedBytes = cw.toByteArray(); if (VERIFY_WEAVING) { verify(transformedBytes, loader, classBytes, className); } return transformedBytes; } private static void verify(byte[] transformedBytes, @Nullable ClassLoader loader, byte[] originalBytes, String className) { String originalBytesVerifyError = verify(originalBytes, loader); if (!originalBytesVerifyError.isEmpty()) { // not much to do if original byte code fails to verify logger.debug("class verify error for original bytecode\n:" + originalBytesVerifyError); return; } String transformedBytesVerifyError = verify(transformedBytes, loader); if (!transformedBytesVerifyError.isEmpty()) { logger.error( "class verify error for transformed bytecode\n:" + transformedBytesVerifyError); try { File originalBytesFile = getTempFile(className, "glowroot-verify-error-", "-original.class"); Files.write(originalBytes, originalBytesFile); logger.error("wrote original bytecode to: {}", originalBytesFile.getAbsolutePath()); File transformedBytesFile = getTempFile(className, "glowroot-verify-error-", "-transformed.class"); Files.write(transformedBytes, transformedBytesFile); logger.error("wrote transformed bytecode to: {}", transformedBytesFile.getAbsolutePath()); } catch (IOException e) { logger.error(e.getMessage(), e); } } } private static String verify(byte[] bytes, @Nullable ClassLoader loader) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); CheckClassAdapter.verify(new ClassReader(bytes), loader, false, pw); pw.close(); return sw.toString(); } private static File getTempFile(String className, String prefix, String suffix) { String tmpDirProperty = StandardSystemProperty.JAVA_IO_TMPDIR.value(); File tmpDir = tmpDirProperty == null ? new File(".") : new File(tmpDirProperty); String simpleName; int index = className.lastIndexOf('/'); if (index == -1) { simpleName = className; } else { simpleName = className.substring(index + 1); } return new File(tmpDir, prefix + simpleName + suffix); } private static class JSRInlinerClassVisitor extends ClassVisitor { private JSRInlinerClassVisitor(ClassVisitor cv) { super(ASM5, cv); } @Override public MethodVisitor visitMethod(int access, String name, String desc, @Nullable String signature, String/*@Nullable*/[] exceptions) { MethodVisitor mv = checkNotNull(cv).visitMethod(access, name, desc, signature, exceptions); return new JSRInlinerAdapter(mv, access, name, desc, signature, exceptions); } } @VisibleForTesting static class ComputeFramesClassWriter extends ClassWriter { private final AnalyzedWorld analyzedWorld; private final @Nullable ClassLoader loader; private final ParseContext parseContext; public ComputeFramesClassWriter(int flags, AnalyzedWorld analyzedWorld, @Nullable ClassLoader loader, @Nullable CodeSource codeSource, String className) { super(flags); this.analyzedWorld = analyzedWorld; this.loader = loader; this.parseContext = ImmutableParseContext.of(className, codeSource); } // implements logic similar to org.objectweb.asm.ClassWriter.getCommonSuperClass() @Override protected String getCommonSuperClass(String type1, String type2) { if (type1.equals("java/lang/Object") || type2.equals("java/lang/Object")) { return "java/lang/Object"; } try { return getCommonSuperClassInternal(type1, type2); } catch (IOException e) { logger.error(e.getMessage(), e); return "java/lang/Object"; } } private String getCommonSuperClassInternal(String type1, String type2) throws IOException { AnalyzedClass analyzedClass1; try { analyzedClass1 = analyzedWorld.getAnalyzedClass(ClassNames.fromInternalName(type1), loader); } catch (ClassNotFoundException e) { // log at debug level only since this code will fail anyways if it is actually used // at runtime since type doesn't exist logger.debug("type {} not found while parsing type {}", type1, parseContext, e); return "java/lang/Object"; } AnalyzedClass analyzedClass2; try { analyzedClass2 = analyzedWorld.getAnalyzedClass(ClassNames.fromInternalName(type2), loader); } catch (ClassNotFoundException e) { // log at debug level only since this code will fail anyways if it is actually used // at runtime since type doesn't exist logger.debug("type {} not found while parsing type {}", type2, parseContext, e); return "java/lang/Object"; } return getCommonSuperClass(analyzedClass1, analyzedClass2, type1, type2); } private String getCommonSuperClass(AnalyzedClass analyzedClass1, AnalyzedClass analyzedClass2, String type1, String type2) throws IOException { if (isAssignableFrom(analyzedClass1.name(), analyzedClass2)) { return type1; } if (isAssignableFrom(analyzedClass2.name(), analyzedClass1)) { return type2; } if (analyzedClass1.isInterface() || analyzedClass2.isInterface()) { return "java/lang/Object"; } return getCommonSuperClass(analyzedClass1, analyzedClass2); } private String getCommonSuperClass(AnalyzedClass analyzedClass1, AnalyzedClass analyzedClass2) throws IOException { // climb analyzedClass1 super class hierarchy and check if any of them are assignable // from analyzedClass2 String superName = analyzedClass1.superName(); while (superName != null) { if (isAssignableFrom(superName, analyzedClass2)) { return ClassNames.toInternalName(superName); } try { AnalyzedClass superAnalyzedClass = analyzedWorld.getAnalyzedClass(superName, loader); superName = superAnalyzedClass.superName(); } catch (ClassNotFoundException e) { // log at debug level only since this code must not be getting used anyways, as // it would fail on execution since the type doesn't exist logger.debug("type {} not found while parsing type {}", superName, parseContext, e); return "java/lang/Object"; } } return "java/lang/Object"; } private boolean isAssignableFrom(String possibleSuperClassName, AnalyzedClass analyzedClass) throws IOException { if (analyzedClass.name().equals(possibleSuperClassName)) { return true; } if (isAssignableFromInterfaces(possibleSuperClassName, analyzedClass)) { return true; } String superName = analyzedClass.superName(); if (superName == null) { return false; } return isAssignableFromSuperClass(possibleSuperClassName, superName); } private boolean isAssignableFromInterfaces(String possibleSuperClassName, AnalyzedClass analyzedClass) throws IOException { for (String interfaceName : analyzedClass.interfaceNames()) { try { AnalyzedClass interfaceAnalyzedClass = analyzedWorld.getAnalyzedClass(interfaceName, loader); if (isAssignableFrom(possibleSuperClassName, interfaceAnalyzedClass)) { return true; } } catch (ClassNotFoundException e) { // log at debug level only since this code must not be getting used anyways, as // it would fail on execution since the type doesn't exist logger.debug("type {} not found while parsing type {}", interfaceName, parseContext, e); } } return false; } private boolean isAssignableFromSuperClass(String possibleSuperClassName, String superName) throws IOException { try { AnalyzedClass superAnalyzedClass = analyzedWorld.getAnalyzedClass(superName, loader); return isAssignableFrom(possibleSuperClassName, superAnalyzedClass); } catch (ClassNotFoundException e) { // log at debug level only since this code must not be getting used anyways, as it // would fail on execution since the type doesn't exist logger.debug("type {} not found while parsing type {}", superName, parseContext, e); return false; } } } private static class FelixOsgiHackClassVisitor extends ClassVisitor { private final ClassWriter cw; FelixOsgiHackClassVisitor(ClassWriter cw) { super(ASM5, cw); this.cw = cw; } @Override public MethodVisitor visitMethod(int access, String name, String desc, @Nullable String signature, String /*@Nullable*/[] exceptions) { MethodVisitor mv = cw.visitMethod(access, name, desc, signature, exceptions); if (name.equals("shouldBootDelegate") && desc.equals("(Ljava/lang/String;)Z")) { return new FelixOsgiHackMethodVisitor(mv, access, name, desc); } else { return mv; } } } private static class FelixOsgiHackMethodVisitor extends AdviceAdapter { private FelixOsgiHackMethodVisitor(MethodVisitor mv, int access, String name, String desc) { super(ASM5, mv, access, name, desc); } @Override protected void onMethodEnter() { mv.visitVarInsn(ALOAD, 1); mv.visitLdcInsn("org.glowroot."); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "startsWith", "(Ljava/lang/String;)Z", false); Label label = new Label(); mv.visitJumpInsn(IFEQ, label); mv.visitInsn(ICONST_1); mv.visitInsn(IRETURN); mv.visitLabel(label); } } private static class JBoss4HackClassVisitor extends ClassVisitor { private final ClassWriter cw; JBoss4HackClassVisitor(ClassWriter cw) { super(ASM5, cw); this.cw = cw; } @Override public MethodVisitor visitMethod(int access, String name, String desc, @Nullable String signature, String /*@Nullable*/[] exceptions) { MethodVisitor mv = cw.visitMethod(access, name, desc, signature, exceptions); if (name.equals("internalInitURLHandlers") && desc.equals("()V")) { return new JBoss4HackMethodVisitor(mv, access, name, desc); } else { return mv; } } } private static class JBoss4HackMethodVisitor extends AdviceAdapter { private JBoss4HackMethodVisitor(MethodVisitor mv, int access, String name, String desc) { super(ASM5, mv, access, name, desc); } @Override protected void onMethodEnter() { // these classes can be initialized inside of ClassFileTransformer.transform(), via // Resources.toByteArray(url) inside of AnalyzedWorld.createAnalyzedClass() // because jboss 4.x registers org.jboss.net.protocol.URLStreamHandlerFactory to handle // "file" and "resource" URLs // // these classes can not be initialized in PreInitializeWeavingClasses since they are // not accessible from the bootstrap or system class loader, and thus, this hack Label l0 = new Label(); Label l1 = new Label(); Label l2 = new Label(); mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Throwable"); mv.visitLabel(l0); visitClassForName("org.jboss.net.protocol.file.Handler"); visitClassForName("org.jboss.net.protocol.file.FileURLConnection"); visitClassForName("org.jboss.net.protocol.resource.Handler"); visitClassForName("org.jboss.net.protocol.resource.ResourceURLConnection"); mv.visitLabel(l1); Label l3 = new Label(); mv.visitJumpInsn(GOTO, l3); mv.visitLabel(l2); if (logger.isDebugEnabled()) { mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Throwable", "printStackTrace", "()V", false); } else { mv.visitInsn(POP); } mv.visitLabel(l3); } private void visitClassForName(String className) { mv.visitLdcInsn(className); mv.visitMethodInsn(INVOKESTATIC, "java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;", false); mv.visitInsn(POP); } } @Pointcut(className = "", methodName = "", methodParameterTypes = {}, timerName = "glowroot weaving") private static class OnlyForTheTimerName { private OnlyForTheTimerName() {} } }