Skip to content

Commit

Permalink
Experimental java.io.Closeable debugging
Browse files Browse the repository at this point in the history
  • Loading branch information
PokeMMO committed Mar 25, 2022
1 parent 02b2913 commit 2a63529
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 191 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Java Agent for debugging common libgdx related issues.

* UNDISPOSED (-Dgdxdbg.debug.undisposed=true)
- Enables debugging if a Disposable object is finalized without being properly disposed.
* DOUBLE_DISPOSE (-Dgdxdbg.debug.double_dispose=false)
- Enables debugging if a dispose method is called multiple times.
- Not recommended to use, double dispose calls should be made safe if not already.
* DEBUG_UNCLOSED (-Dgdxdbg.debug.unclosed=true)
- Enables debugging if a java.io.Closeable object is finalized without being properly disposed.
- Cannot currently debug classes which are loaded before java agent is loaded. :(
* MODIFIABLE_CONSTANTS (-Dgdxdbg.debug.modifiable_constants=true)
- Enables debugging if certain `modifiable constant`'s values change during runtime.
- Things constants like Color.WHITE can be accidently modified, leading to unexpected results.
Expand Down
191 changes: 8 additions & 183 deletions src/main/java/gdxdbg/DgbAgentClassFileTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,29 @@
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;

import javassist.CannotCompileException;
import gdxdbg.DisposableTransformer.ModifiedCloseable;
import gdxdbg.DisposableTransformer.ModifiedDisposible;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;

/**
* {@link ClassFileTransformer} which performs class transformations to add various debugging utilities.
* @author Desu
*/
public class DgbAgentClassFileTransformer implements ClassFileTransformer
{
public static final String FIELD_NAME_EXCEPTION = "$$$disposeexception";
public static final String FIELD_NAME_EXCEPTION_DOUBLE_DISPOSE = "$$$doubledisposeexception";

private DisposableTransformer disposableTransformer = new DisposableTransformer("dispose", "$$$disposeexception", "com.badlogic.gdx.utils.Disposable", ModifiedDisposible.class);
private DisposableTransformer closeableTransformer = new DisposableTransformer("close", "$$$closeeexception", "java.io.Closeable", ModifiedCloseable.class);
private ModifiableConstants modifiableConstants = new ModifiableConstants();
private GLThreadVerification glThreadVerification = new GLThreadVerification();

private static CtClass[] EMPTY_CT_CLASSES = new CtClass[0];

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException
{
if(className.startsWith("java/") || className.startsWith("javax/"))
if(!Properties.DEBUG_UNCLOSED && (className.startsWith("java/") || className.startsWith("javax/")))
return null;

try
Expand Down Expand Up @@ -62,49 +55,13 @@ protected byte[] transformClass(ClassLoader loader, byte[] clazzBytes) throws Ex
}

if(glThreadVerification.transform(clazz))
{
modified = true;
}

boolean isDisposable = false;
try
{
CtClass disposable = cp.get("com.badlogic.gdx.utils.Disposable");
isDisposable = isDisposable(clazz, disposable);
}
catch(NotFoundException e)
{
// Don't care
}

boolean foundValidDisposeMethod = false;
if(isDisposable)
{
try
{
CtMethod method = clazz.getDeclaredMethod("dispose", EMPTY_CT_CLASSES);
foundValidDisposeMethod = isDisposible(method);
}
catch(NotFoundException e)
{
// Don't care
}
}
if(Properties.DEBUG_UNDISPOSED && disposableTransformer.transform(cp, clazz))
modified = true;

if(foundValidDisposeMethod)
{
CtClass eo = cp.get(ModifiedDisposible.class.getName());
for(CtClass i : clazz.getInterfaces())
{
if(i.equals(eo))
{
throw new RuntimeException("Class already implements ModifiedDisposible interface");
}
}

writeDisposibleObjectImpl(clazz);
if(Properties.DEBUG_UNCLOSED && closeableTransformer.transform(cp, clazz))
modified = true;
}

if(modified)
{
Expand All @@ -115,136 +72,4 @@ protected byte[] transformClass(ClassLoader loader, byte[] clazzBytes) throws Ex

return null;
}

private boolean isDisposable(CtClass clazz, CtClass disposable) throws Exception
{
if(clazz == null)
return false;

if(clazz.getName().startsWith("com.badlogic.gdx.graphics.g3d.particles.influencers.DynamicsModifier") || clazz.getName().startsWith("com.badlogic.gdx.graphics.g3d.particles.ParticleControllerComponent"))
return false;

// These aren't necessarily required to be disposed, and their sub resources will warn us of issues
if(clazz.getName().startsWith("com.badlogic.gdx.graphics.g2d.PixmapPacker") || clazz.getName().startsWith("com.badlogic.gdx.maps.Map"))
return false;

if(clazz.equals(disposable))
return true;

for(CtClass i : clazz.getInterfaces())
{
if(isDisposable(i, disposable))
return true;
}

if(isDisposable(clazz.getSuperclass(), disposable))
return true;

return false;
}

protected void writeDisposibleObjectImpl(CtClass clazz) throws NotFoundException, CannotCompileException
{
ClassPool cp = clazz.getClassPool();
clazz.addInterface(cp.get(ModifiedDisposible.class.getName()));
writeDisposibleObjectFields(clazz);
writeDisposibleObjectMethods(clazz);
}

private void writeDisposibleObjectFields(CtClass clazz) throws CannotCompileException, NotFoundException
{
ClassPool cp = clazz.getClassPool();

if(Properties.DEBUG_UNDISPOSED)
{
// add object that holds path from initialization
CtField cbField = new CtField(cp.get(RuntimeException.class.getName()), FIELD_NAME_EXCEPTION, clazz);
cbField.setModifiers(Modifier.PRIVATE | Modifier.TRANSIENT);
clazz.addField(cbField, CtField.Initializer.byExpr("new RuntimeException(\"Undisposed \"+getClass().getName()+\" resource\");"));
}

if(Properties.DEBUG_DOUBLE_DISPOSE)
{
// add object that holds path from dispose for double dispose calls
CtField cbField = new CtField(cp.get(RuntimeException.class.getName()), FIELD_NAME_EXCEPTION_DOUBLE_DISPOSE, clazz);
cbField.setModifiers(Modifier.PRIVATE | Modifier.TRANSIENT);
clazz.addField(cbField);
}
}

private void writeDisposibleObjectMethods(CtClass clazz) throws NotFoundException, CannotCompileException
{
if(Properties.DEBUG_UNDISPOSED)
{
CtMethod meth = null;
try
{
meth = clazz.getDeclaredMethod("finalize", EMPTY_CT_CLASSES);
}
catch(NotFoundException e)
{
//Ignored
}

StringBuilder sb = new StringBuilder();
if(clazz.getName().equals("com.badlogic.gdx.graphics.GLTexture"))
{
sb.append("if((this instanceof com.badlogic.gdx.graphics.Texture) || (this instanceof com.badlogic.gdx.graphics.Cubemap)){} else ");
}
sb.append("if("+FIELD_NAME_EXCEPTION+" != null) "+FIELD_NAME_EXCEPTION+".printStackTrace();");

if(meth == null)
{
if(Properties.TRACE)
System.out.println("finalize not found creating it");
CtMethod m = CtNewMethod.make("public void finalize() throws Throwable { "+sb.toString()+"; super.finalize(); } ", clazz);
clazz.addMethod(m);
}
else
{
if(Properties.TRACE)
System.out.println("modifing " + clazz.getName() + "'s finalize method");
meth.insertBefore("{ "+sb.toString()+" }");
}
}

if(Properties.DEBUG_UNDISPOSED || Properties.DEBUG_DOUBLE_DISPOSE)
{
CtMethod meth = null;
try
{
meth = clazz.getDeclaredMethod("dispose", EMPTY_CT_CLASSES);
}
catch(NotFoundException e)
{
//Ignored
}

if(meth != null)
{
if(Properties.DEBUG_UNDISPOSED)
meth.insertBefore("{ "+FIELD_NAME_EXCEPTION+" = null; }");

// Some are safe to double dispose
if(Properties.DEBUG_DOUBLE_DISPOSE && !"com.badlogic.gdx.graphics.Texture".equals(clazz.getName()))
meth.insertBefore("{ synchronized(this) { if("+FIELD_NAME_EXCEPTION_DOUBLE_DISPOSE+" != null) new RuntimeException(\"Double Dispose\", "+FIELD_NAME_EXCEPTION_DOUBLE_DISPOSE+").printStackTrace(); "
+ FIELD_NAME_EXCEPTION_DOUBLE_DISPOSE+" = new RuntimeException(\"Previous Dispose\");"
+ "} }");
}
}
}

protected boolean isDisposible(CtMethod method)
{
int modifiers = method.getModifiers();
if(Modifier.isAbstract(modifiers) || Modifier.isNative(modifiers) || Modifier.isStatic(modifiers))
return false;

return method.getName().equals("dispose");
}

public static interface ModifiedDisposible
{

}
}
Loading

0 comments on commit 2a63529

Please sign in to comment.