src.main.java.com.mebigfatguy.fbcontrib.detect.HangingExecutors Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fb-contrib Show documentation
Show all versions of fb-contrib Show documentation
An auxiliary findbugs.sourceforge.net plugin for java bug detectors that fall outside the narrow scope of detectors to be packaged with the product itself.
/*
* fb-contrib - Auxiliary detectors for Java programs
* Copyright (C) 2005-2018 Kevin Lubick
* Copyright (C) 2005-2018 Dave Brosius
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.mebigfatguy.fbcontrib.detect;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.SignatureUtils;
import com.mebigfatguy.fbcontrib.utils.ToString;
import com.mebigfatguy.fbcontrib.utils.UnmodifiableSet;
import com.mebigfatguy.fbcontrib.utils.Values;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.BytecodeScanningDetector;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.FieldAnnotation;
import edu.umd.cs.findbugs.OpcodeStack;
import edu.umd.cs.findbugs.Priorities;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.XFactory;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.ba.XMethod;
/**
* looks for executors that are never shutdown, which will not allow the application to terminate
*/
public class HangingExecutors extends BytecodeScanningDetector {
private static final Set hangableSig = UnmodifiableSet.create("Ljava/util/concurrent/ExecutorService;",
"Ljava/util/concurrent/AbstractExecutorService;", "Ljava/util/concurrent/ForkJoinPool;", "Ljava/util/concurrent/ScheduledThreadPoolExecutor;",
"Ljava/util/concurrent/ThreadPoolExecutor;");
private static final Set shutdownMethods = UnmodifiableSet.create("shutdown", "shutdownNow");
private final BugReporter bugReporter;
private Map hangingFieldCandidates;
private Map exemptExecutors;
private OpcodeStack stack;
private String methodName;
private boolean isInitializer;
private final LocalHangingExecutor localHEDetector;
public HangingExecutors(BugReporter reporter) {
this.bugReporter = reporter;
this.localHEDetector = new LocalHangingExecutor(this, reporter);
}
/**
* finds ExecutorService objects that don't get a call to the terminating methods, and thus, never appear to be shutdown properly (the threads exist until
* shutdown is called)
*
* @param classContext
* the class context object of the currently parsed java class
*/
@Override
public void visitClassContext(ClassContext classContext) {
localHEDetector.visitClassContext(classContext);
try {
hangingFieldCandidates = new HashMap<>();
exemptExecutors = new HashMap<>();
parseFieldsForHangingCandidates(classContext);
if (!hangingFieldCandidates.isEmpty()) {
stack = new OpcodeStack();
super.visitClassContext(classContext);
reportHangingExecutorFieldBugs();
}
} finally {
stack = null;
hangingFieldCandidates = null;
exemptExecutors = null;
}
}
private void parseFieldsForHangingCandidates(ClassContext classContext) {
JavaClass cls = classContext.getJavaClass();
Field[] fields = cls.getFields();
for (Field f : fields) {
String sig = f.getSignature();
if (hangableSig.contains(sig)) {
hangingFieldCandidates.put(XFactory.createXField(cls.getClassName(), f.getName(), f.getSignature(), f.isStatic()),
new AnnotationPriority(FieldAnnotation.fromBCELField(cls, f), NORMAL_PRIORITY));
}
}
}
private void reportHangingExecutorFieldBugs() {
for (Entry entry : hangingFieldCandidates.entrySet()) {
AnnotationPriority fieldAn = entry.getValue();
if (fieldAn != null) {
bugReporter.reportBug(new BugInstance(this, BugType.HES_EXECUTOR_NEVER_SHUTDOWN.name(), fieldAn.priority).addClass(this)
.addField(fieldAn.annotation).addField(entry.getKey()));
}
}
}
/**
* implements the visitor to reset the opcode stack
*
* @param obj
* the context object of the currently parsed code block
*/
@Override
public void visitCode(Code obj) {
stack.resetForMethodEntry(this);
exemptExecutors.clear();
if (!hangingFieldCandidates.isEmpty()) {
isInitializer = (Values.STATIC_INITIALIZER.equals(methodName) || Values.CONSTRUCTOR.equals(methodName));
super.visitCode(obj);
}
}
/**
* implements the visitor to collect the method name
*
* @param obj
* the context object of the currently parsed method
*/
@Override
public void visitMethod(Method obj) {
methodName = obj.getName();
}
/**
* Browses for calls to shutdown() and shutdownNow(), and if they happen, remove the hanging candidate, as there is a chance it will be called.
*
* @param seen
* the opcode of the currently parsed instruction
*/
@Override
public void sawOpcode(int seen) {
if (isInitializer) {
lookForCustomThreadFactoriesInConstructors(seen);
return;
}
try {
stack.precomputation(this);
if ((seen == INVOKEVIRTUAL) || (seen == INVOKEINTERFACE)) {
processInvoke();
}
// TODO Should not include private methods
else if (seen == ARETURN) {
removeFieldsThatGetReturned();
} else if (seen == PUTFIELD) {
XField f = getXFieldOperand();
if (f != null) {
reportOverwrittenField(f);
}
} else if (seen == IFNONNULL) {
// indicates a null check, which means that we get an exemption
// until the end of the branch
OpcodeStack.Item nullCheckItem = stack.getStackItem(0);
XField fieldWhichWasNullChecked = nullCheckItem.getXField();
if (fieldWhichWasNullChecked != null) {
exemptExecutors.put(fieldWhichWasNullChecked, Integer.valueOf(getPC() + getBranchOffset()));
}
}
} finally {
stack.sawOpcode(this, seen);
}
}
private void processInvoke() {
String sig = getSigConstantOperand();
int argCount = SignatureUtils.getNumParameters(sig);
if (stack.getStackDepth() > argCount) {
OpcodeStack.Item invokeeItem = stack.getStackItem(argCount);
XField fieldOnWhichMethodIsInvoked = invokeeItem.getXField();
if (fieldOnWhichMethodIsInvoked != null) {
removeCandidateIfShutdownCalled(fieldOnWhichMethodIsInvoked);
addExemptionIfShutdownCalled(fieldOnWhichMethodIsInvoked);
}
}
}
private void lookForCustomThreadFactoriesInConstructors(int seen) {
try {
stack.precomputation(this);
if (seen == PUTFIELD) {
XField f = getXFieldOperand();
if ((f != null) && hangableSig.contains(f.getSignature())) {
// look at the top of the stack, get the arguments passed
// into the function that was called
// and then pull out the types.
// if the last type is a ThreadFactory, set the priority to
// low
XMethod method = stack.getStackItem(0).getReturnValueOf();
if (method != null) {
List argumentTypes = SignatureUtils.getParameterSignatures(method.getSignature());
if ((!argumentTypes.isEmpty()) && "Ljava/util/concurrent/ThreadFactory;".equals(argumentTypes.get(argumentTypes.size() - 1))) {
AnnotationPriority ap = this.hangingFieldCandidates.get(f);
if (ap != null) {
ap.priority = LOW_PRIORITY;
this.hangingFieldCandidates.put(f, ap);
}
}
} else {
// if the object is initialized from parameter, it's not this class's job to close it
int reg = stack.getStackItem(0).getRegisterNumber();
if (reg >= 0) {
Map ctorParmInfo = SignatureUtils.getParameterSlotAndSignatures(false, getMethod().getSignature());
if (ctorParmInfo.containsKey(Integer.valueOf(reg))) {
hangingFieldCandidates.remove(f);
}
}
}
}
}
} finally {
stack.sawOpcode(this, seen);
}
}
private void reportOverwrittenField(XField f) {
if ("Ljava/util/concurrent/ExecutorService;".equals(f.getSignature()) && !checkException(f)) {
bugReporter.reportBug(new BugInstance(this, BugType.HES_EXECUTOR_OVERWRITTEN_WITHOUT_SHUTDOWN.name(), Priorities.NORMAL_PRIORITY).addClass(this)
.addMethod(this).addField(f).addSourceLine(this));
}
// after it's been replaced, it no longer uses its exemption.
exemptExecutors.remove(f);
}
private boolean checkException(XField f) {
if (!exemptExecutors.containsKey(f)) {
return false;
}
int i = exemptExecutors.get(f).intValue();
return (i == -1) || (getPC() < i);
}
private void removeFieldsThatGetReturned() {
if (stack.getStackDepth() > 0) {
OpcodeStack.Item returnItem = stack.getStackItem(0); // top thing on
// the stack
// was the
// variable
// being
// returned
XField field = returnItem.getXField();
if (field != null) {
hangingFieldCandidates.remove(field);
}
}
}
private void addExemptionIfShutdownCalled(XField fieldOnWhichMethodIsInvoked) {
String methodBeingInvoked = getNameConstantOperand();
if (shutdownMethods.contains(methodBeingInvoked)) {
exemptExecutors.put(fieldOnWhichMethodIsInvoked, Values.NEGATIVE_ONE);
}
}
private void removeCandidateIfShutdownCalled(XField fieldOnWhichMethodIsInvoked) {
if (hangingFieldCandidates.containsKey(fieldOnWhichMethodIsInvoked)) {
String methodBeingInvoked = getNameConstantOperand();
if (shutdownMethods.contains(methodBeingInvoked)) {
hangingFieldCandidates.remove(fieldOnWhichMethodIsInvoked);
}
}
}
/**
* represents a field that is a executor
*/
private static class AnnotationPriority {
int priority;
FieldAnnotation annotation;
AnnotationPriority(FieldAnnotation annotation, int priority) {
this.annotation = annotation;
this.priority = priority;
}
@Override
public String toString() {
return ToString.build(this);
}
}
}
class LocalHangingExecutor extends LocalTypeDetector {
private static final Map> watchedClassMethods;
private static final Map syncCtors;
static {
Set forExecutors = new HashSet<>();
forExecutors.add("newCachedThreadPool");
forExecutors.add("newFixedThreadPool");
forExecutors.add("newScheduledThreadPool");
forExecutors.add("newSingleThreadExecutor");
Map> wcm = new HashMap<>();
wcm.put("java/util/concurrent/Executors", forExecutors);
watchedClassMethods = Collections.unmodifiableMap(wcm);
Map sc = new HashMap<>();
sc.put("java/util/concurrent/ThreadPoolExecutor", Values.JAVA_5);
sc.put("java/util/concurrent/ScheduledThreadPoolExecutor", Values.JAVA_5);
syncCtors = Collections.unmodifiableMap(sc);
}
private final BugReporter bugReporter;
private final Detector delegatingDetector;
public LocalHangingExecutor(Detector delegatingDetector, BugReporter reporter) {
this.bugReporter = reporter;
this.delegatingDetector = delegatingDetector;
}
@Override
protected Map getWatchedConstructors() {
return syncCtors;
}
@Override
protected Map> getWatchedClassMethods() {
return watchedClassMethods;
}
@Override
protected Set getSelfReturningMethods() {
return Collections.emptySet();
}
@Override
protected void reportBug(RegisterInfo cri) {
// very important to report the bug under the top, parent detector,
// otherwise it gets filtered out
bugReporter.reportBug(new BugInstance(delegatingDetector, "HES_LOCAL_EXECUTOR_SERVICE", LOW_PRIORITY).addClass(this).addMethod(this)
.addSourceLine(cri.getSourceLineAnnotation()));
}
}