org.glowroot.instrumentation.engine.weaving.Weaver Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2012-2019 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.instrumentation.engine.weaving;
import java.io.File;
import java.io.IOException;
import java.lang.management.LockInfo;
import java.lang.management.ManagementFactory;
import java.lang.management.MonitorInfo;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.security.CodeSource;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.base.StandardSystemProperty;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.base.Supplier;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.base.Ticker;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.collect.ImmutableList;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.collect.Lists;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.collect.Sets;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.io.Files;
import org.glowroot.instrumentation.test.harness.shaded.com.google.common.primitives.Longs;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.glowroot.instrumentation.test.harness.shaded.org.objectweb.asm.ClassReader;
import org.glowroot.instrumentation.test.harness.shaded.org.objectweb.asm.ClassVisitor;
import org.glowroot.instrumentation.test.harness.shaded.org.objectweb.asm.ClassWriter;
import org.glowroot.instrumentation.test.harness.shaded.org.objectweb.asm.Label;
import org.glowroot.instrumentation.test.harness.shaded.org.objectweb.asm.MethodVisitor;
import org.glowroot.instrumentation.test.harness.shaded.org.objectweb.asm.Type;
import org.glowroot.instrumentation.test.harness.shaded.org.objectweb.asm.commons.AdviceAdapter;
import org.glowroot.instrumentation.test.harness.shaded.org.slf4j.Logger;
import org.glowroot.instrumentation.test.harness.shaded.org.slf4j.LoggerFactory;
import org.glowroot.instrumentation.engine.util.IterableWithSelfRemovableEntries;
import org.glowroot.instrumentation.engine.util.IterableWithSelfRemovableEntries.SelfRemovableEntry;
import org.glowroot.instrumentation.engine.weaving.ClassLoaders.LazyDefinedClass;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.glowroot.instrumentation.test.harness.shaded.org.objectweb.asm.Opcodes.ASM7;
import static org.glowroot.instrumentation.test.harness.shaded.org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.glowroot.instrumentation.test.harness.shaded.org.objectweb.asm.Opcodes.V1_6;
public class Weaver {
private static final Logger logger = LoggerFactory.getLogger(Weaver.class);
// useful for debugging java.lang.VerifyError and java.lang.ClassFormatError
private static final @Nullable String DEBUG_CLASS_NAME;
static {
String debugClassName = System.getProperty("glowroot.debug.className");
if (debugClassName == null) {
DEBUG_CLASS_NAME = null;
} else {
DEBUG_CLASS_NAME = ClassNames.toInternalName(debugClassName);
}
}
private final Supplier> advisors;
private final ImmutableList shimTypes;
private final ImmutableList mixinTypes;
private final AnalyzedWorld analyzedWorld;
private final Ticker ticker;
private volatile boolean noLongerNeedToWeaveMainMethods;
private volatile boolean weavingDisabledForLoggingDeadlock;
private final IterableWithSelfRemovableEntries activeWeavings =
new IterableWithSelfRemovableEntries();
public Weaver(Supplier> advisors, List shimTypes,
List mixinTypes, AnalyzedWorld analyzedWorld, Ticker ticker) {
this.advisors = advisors;
this.shimTypes = ImmutableList.copyOf(shimTypes);
this.mixinTypes = ImmutableList.copyOf(mixinTypes);
this.analyzedWorld = analyzedWorld;
this.ticker = ticker;
}
public void setNoLongerNeedToWeaveMainMethods() {
noLongerNeedToWeaveMainMethods = true;
}
public boolean checkForDeadlockedActiveWeaving() {
long currTick = ticker.read();
List threadIds = Lists.newArrayList();
for (ActiveWeaving activeWeaving : activeWeavings) {
if (NANOSECONDS.toSeconds(currTick - activeWeaving.startTick) > 5) {
threadIds.add(activeWeaving.threadId);
}
}
if (threadIds.isEmpty()) {
return false;
} else {
return checkForDeadlockedActiveWeaving(threadIds);
}
}
byte /*@Nullable*/ [] weave(byte[] classBytes, String className,
@Nullable Class> classBeingRedefined, @Nullable CodeSource codeSource,
@Nullable ClassLoader loader) {
if (weavingDisabledForLoggingDeadlock) {
return null;
}
long startTick = ticker.read();
SelfRemovableEntry activeWeavingEntry =
activeWeavings.add(new ActiveWeaving(Thread.currentThread().getId(), startTick));
try {
logger.trace("transform(): className={}", className);
byte[] transformedBytes =
weaveUnderTimer(classBytes, className, classBeingRedefined, codeSource, loader);
if (transformedBytes != null) {
logger.debug("transform(): transformed {}", className);
}
return transformedBytes;
} finally {
activeWeavingEntry.remove();
}
}
private byte /*@Nullable*/ [] weaveUnderTimer(byte[] classBytes, String className,
@Nullable Class> classBeingRedefined, @Nullable CodeSource codeSource,
@Nullable ClassLoader loader) {
List advisors = analyzedWorld.mergeInstrumentationAnnotations(this.advisors.get(),
classBytes, loader, className);
ThinClassVisitor accv = new ThinClassVisitor();
new ClassReader(classBytes).accept(accv, ClassReader.SKIP_FRAMES + ClassReader.SKIP_CODE);
boolean frames = accv.getMajorVersion() >= V1_6;
int parsingOptions = frames ? ClassReader.EXPAND_FRAMES : ClassReader.SKIP_FRAMES;
byte[] maybeProcessedBytes = null;
if (className.equals(ImportantClassNames.JBOSS_WELD_HACK_CLASS_NAME)) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new JBossWeldHackClassVisitor(cw);
ClassReader cr = new ClassReader(classBytes);
cr.accept(new JSRInlinerClassVisitor(cv), parsingOptions);
maybeProcessedBytes = cw.toByteArray();
} else if (className.equals(ImportantClassNames.JBOSS_URL_HACK_CLASS_NAME)) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new JBossUrlHackClassVisitor(cw);
ClassReader cr = new ClassReader(classBytes);
cr.accept(new JSRInlinerClassVisitor(cv), parsingOptions);
maybeProcessedBytes = cw.toByteArray();
} else if (className.equals("java/lang/ClassLoader")) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new ClassLoaderHackClassVisitor(cw);
ClassReader cr = new ClassReader(classBytes);
cr.accept(new JSRInlinerClassVisitor(cv), parsingOptions);
maybeProcessedBytes = cw.toByteArray();
}
ClassAnalyzer classAnalyzer = new ClassAnalyzer(accv.getThinClass(), advisors, shimTypes,
mixinTypes, loader, analyzedWorld, codeSource, classBytes, classBeingRedefined,
noLongerNeedToWeaveMainMethods);
try {
classAnalyzer.analyzeMethods();
} catch (ClassNotFoundException e) {
logger.error("error analyzing {}: {}", className, e.getMessage(), e);
return null;
} catch (IOException e) {
logger.error("error analyzing {}: {}", className, e.getMessage(), e);
return null;
}
if (!classAnalyzer.isWeavingRequired()) {
analyzedWorld.add(classAnalyzer.getAnalyzedClass(), loader);
return maybeProcessedBytes;
}
List matchedShimTypes = classAnalyzer.getMatchedShimTypes();
List reweavableMatchedMixinTypes =
classAnalyzer.getMatchedReweavableMixinTypes();
if (classBeingRedefined != null
&& (!matchedShimTypes.isEmpty() || !reweavableMatchedMixinTypes.isEmpty())) {
Set interfaceNames = Sets.newHashSet();
for (Class> iface : classBeingRedefined.getInterfaces()) {
interfaceNames.add(iface.getName());
}
matchedShimTypes = Lists.newArrayList(matchedShimTypes);
for (ShimType matchedShimType : matchedShimTypes) {
if (!interfaceNames.contains(matchedShimType.iface().getClassName())) {
// re-weaving would fail with "attempted to change superclass or interfaces"
logger.error("not reweaving {} because cannot add shim type: {}",
ClassNames.fromInternalName(className),
matchedShimType.iface().getClassName());
return null;
}
}
for (MixinType matchedMixinType : reweavableMatchedMixinTypes) {
for (Type mixinInterface : matchedMixinType.interfaces()) {
if (!interfaceNames.contains(mixinInterface.getClassName())) {
// re-weaving would fail with "attempted to change superclass or interfaces"
logger.error("not reweaving {} because cannot add mixin type: {}",
ClassNames.fromInternalName(className),
mixinInterface.getClassName());
return null;
}
}
}
}
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
WeavingClassVisitor cv = new WeavingClassVisitor(cw, loader, frames,
noLongerNeedToWeaveMainMethods, classAnalyzer.getAnalyzedClass(),
classAnalyzer.isClassLoader(), classAnalyzer.getMethodsThatOnlyNowFulfillAdvice(),
matchedShimTypes, reweavableMatchedMixinTypes, classAnalyzer.getMethodAdvisors(),
analyzedWorld);
ClassReader cr =
new ClassReader(maybeProcessedBytes == null ? classBytes : maybeProcessedBytes);
byte[] transformedBytes;
try {
cr.accept(new JSRInlinerClassVisitor(cv), parsingOptions);
// ClassWriter.toByteArray() can throw exception also, see issue #370
transformedBytes = cw.toByteArray();
} catch (RuntimeException e) {
logger.error("unable to weave {}: {}", className, e.getMessage(), e);
try {
File tempFile = getTempFile(className, "instrumentation-error-", ".class");
Files.write(classBytes, tempFile);
logger.error("wrote bytecode to: {}", tempFile.getAbsolutePath());
} catch (IOException f) {
logger.error(f.getMessage(), f);
}
return null;
}
if (className.equals(DEBUG_CLASS_NAME)) {
try {
String name = className.replace('/', '.');
File tmpDir = new File(System.getProperty("java.io.tmpdir"));
File originalClassFile = new File(tmpDir, name + "-original.class");
Files.write(classBytes, originalClassFile);
logger.info("original class file for {} written to: {}", name,
originalClassFile.getAbsolutePath());
File transformedClassFile = new File(tmpDir, name + "-transformed.class");
Files.write(transformedBytes, transformedClassFile);
logger.info("transformed class file for {} written to: {}", name,
transformedClassFile.getAbsolutePath());
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
if (loader != null) {
try {
for (Advice usedAdvice : cv.getUsedAdvisors()) {
LazyDefinedClass nonBootstrapLoaderAdviceClass =
usedAdvice.nonBootstrapLoaderAdviceClass();
if (nonBootstrapLoaderAdviceClass != null) {
ClassLoaders.defineClassIfNotExists(nonBootstrapLoaderAdviceClass, loader);
}
}
} catch (Exception e) {
logger.error("unable to weave {}: {}", className, e.getMessage(), e);
return null;
}
}
return transformedBytes;
}
private boolean checkForDeadlockedActiveWeaving(List activeWeavingThreadIds) {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreadIds = threadBean.findDeadlockedThreads();
if (deadlockedThreadIds == null || Collections.disjoint(Longs.asList(deadlockedThreadIds),
activeWeavingThreadIds)) {
return false;
}
// need to disable weaving, otherwise getThreadInfo can trigger class loading and itself get
// blocked by the deadlocked threads
weavingDisabledForLoggingDeadlock = true;
try {
@Nullable
ThreadInfo[] threadInfos = threadBean.getThreadInfo(deadlockedThreadIds,
threadBean.isObjectMonitorUsageSupported(), false);
StringBuilder sb = new StringBuilder();
for (ThreadInfo threadInfo : threadInfos) {
if (threadInfo != null) {
sb.append('\n');
appendThreadInfo(sb, threadInfo);
}
}
logger.error("deadlock detected in class weaving, please report this to"
+ " https://github.com/glowroot/instrumentation:\n{}", sb);
return true;
} finally {
weavingDisabledForLoggingDeadlock = false;
}
}
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 void appendThreadInfo(StringBuilder sb, ThreadInfo threadInfo) {
sb.append('"');
sb.append(threadInfo.getThreadName());
sb.append("\" #");
sb.append(threadInfo.getThreadId());
sb.append("\n java.lang.Thread.State: ");
sb.append(threadInfo.getThreadState().name());
sb.append('\n');
LockInfo lockInfo = threadInfo.getLockInfo();
StackTraceElement[] stackTrace = threadInfo.getStackTrace();
for (int i = 0; i < stackTrace.length; i++) {
StackTraceElement stackTraceElement = stackTrace[i];
sb.append(" ");
sb.append(stackTraceElement);
sb.append('\n');
if (i == 0 && lockInfo != null) {
Thread.State threadState = threadInfo.getThreadState();
switch (threadState) {
case BLOCKED:
sb.append(" - blocked on ");
sb.append(lockInfo);
sb.append('\n');
break;
case WAITING:
case TIMED_WAITING:
sb.append(" - waiting on ");
sb.append(lockInfo);
sb.append('\n');
break;
default:
break;
}
}
for (MonitorInfo monitorInfo : threadInfo.getLockedMonitors()) {
if (monitorInfo.getLockedStackDepth() == i) {
sb.append(" - locked ");
sb.append(monitorInfo);
sb.append('\n');
}
}
}
}
private static class ActiveWeaving {
private final long threadId;
private final long startTick;
private ActiveWeaving(long threadId, long startTick) {
this.threadId = threadId;
this.startTick = startTick;
}
}
private static class JBossWeldHackClassVisitor extends ClassVisitor {
private final ClassWriter cw;
private JBossWeldHackClassVisitor(ClassWriter cw) {
super(ASM7, cw);
this.cw = cw;
}
@Override
public @Nullable MethodVisitor visitMethod(int access, String name, String descriptor,
@Nullable String signature, String /*@Nullable*/ [] exceptions) {
MethodVisitor mv = cw.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equals("checkDelegateType")
&& descriptor.equals("(Ljavax/enterprise/inject/spi/Decorator;)V")) {
return new JBossWeldHackMethodVisitor(mv);
} else {
return mv;
}
}
}
private static class JBossWeldHackMethodVisitor extends MethodVisitor {
private JBossWeldHackMethodVisitor(MethodVisitor mv) {
super(ASM7, mv);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor,
boolean itf) {
super.visitMethodInsn(opcode, owner, name, descriptor, itf);
if (name.equals("getDecoratedTypes") && descriptor.equals("()Ljava/util/Set;")) {
super.visitMethodInsn(INVOKESTATIC,
"org/glowroot/instrumentation/engine/bytecode/api/Util",
"stripMixinInterfaces", "(Ljava/util/Set;)Ljava/util/Set;", false);
}
}
}
private static class JBossUrlHackClassVisitor extends ClassVisitor {
private final ClassWriter cw;
private JBossUrlHackClassVisitor(ClassWriter cw) {
super(ASM7, cw);
this.cw = cw;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
@Nullable String signature, String /*@Nullable*/ [] exceptions) {
MethodVisitor mv = cw.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equals("") && descriptor.equals("()V")) {
return new JBossUrlHackMethodVisitor(mv, access, name, descriptor);
} else {
return mv;
}
}
}
private static class JBossUrlHackMethodVisitor extends AdviceAdapter {
private JBossUrlHackMethodVisitor(MethodVisitor mv, int access, String name,
String descriptor) {
super(ASM7, mv, access, name, descriptor);
}
@Override
protected void onMethodEnter() {
// these classes can be initialized inside of ClassFileTransformer.transform(), via
// Resources.toByteArray(url) inside of AnalyzedWorld.createAnalyzedClass()
// because jboss 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);
}
}
private static class ClassLoaderHackClassVisitor extends ClassVisitor {
private final ClassWriter cw;
private ClassLoaderHackClassVisitor(ClassWriter cw) {
super(ASM7, cw);
this.cw = cw;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
@Nullable String signature, String /*@Nullable*/ [] exceptions) {
MethodVisitor mv = cw.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equals("defineClass") && descriptor.equals("(Ljava/lang/String;[BII"
+ "Ljava/security/ProtectionDomain;)Ljava/lang/Class;")) {
return new ClassLoaderHackMethodVisitor(mv, access, name, descriptor);
} else {
return mv;
}
}
}
private static class ClassLoaderHackMethodVisitor extends AdviceAdapter {
private ClassLoaderHackMethodVisitor(MethodVisitor mv, int access, String name,
String descriptor) {
super(ASM7, mv, access, name, descriptor);
}
@Override
protected void onMethodEnter() {
visitVarInsn(ALOAD, 0);
visitVarInsn(ALOAD, 1);
visitMethodInsn(INVOKESTATIC,
"org/glowroot/instrumentation/engine/bytecode/api/Bytecode",
"preloadSomeSuperTypes", "(Ljava/lang/ClassLoader;Ljava/lang/String;)V", false);
}
}
}