Eine Art eigener Debugger

Levin Beicht

Mitglied
Hallo,

ich habe folgendes Problem:

In meinem Programm lade ich User-geschriebenen Code mittels des Reflection-APIs und führe diesen Code aus (also eine Art sehr simpler IDE). So jetzt möchte ich aber nicht einfach eine Methode in der User-Klasse aufrufen (wie ich es bisher mache), sondern Zeile für Zeile durch diese Methode durchgehen können und sie Schritt für Schritt ausführen, ähnlich wie es der Debugger in jeder beliebigen IDE kann.

Jetzt ist meine Frage, wie zum Henker mache ich das?
Habe mal versucht den entsprechenden Code im Source von Netbeans zu finden, aber das ist doch alles sehr unübersichtlich da.
Mein nächster Gedanke war das über das Java Debug Interface (JDI) zu machen, aber da werde ich aus der (in meinen Augen recht spärlichen) Doku nicht schlau.

Hat jemand sowas schonmal gemacht und könnte mir einen Tipp geben wie ich das am besten anstelle?
 
Hallo!

Schau dir zuerst mal diese "Einführung" ins JDI an:
http://www.tutorials.de/tutorials207478.html

Anschließend kannst du dir ja mal dieses "Beispiel" anschauen...
(Dazu musst du das Tools.jar in den Classpath aufnehmen -> %JAVA_HOME%/lib)
Wir wollen diese Klasse Debuggen
Java:
package de.tutorials;

public class DebugDummy {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println("1");
		System.out.println("2");
		System.out.println("3");
		System.out.println("4");
		System.out.println("5");
		System.out.println("6");
		System.out.println("7");
	}

}

Diese Klasse führen wir über die Konsole über die Anweisung:
java -agentlib:jdwp=transport=dt_socket,server=y,address=8000 de.tutorials.DebugDummy
aus.

Unseren "Haltepunkt" setzen wir auf den Aufruf der main Methode:
Java:
package de.tutorials;

import java.util.List;
import java.util.Map;

import com.sun.jdi.Bootstrap;
import com.sun.jdi.Location;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.VirtualMachineManager;
import com.sun.jdi.connect.AttachingConnector;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventQueue;
import com.sun.jdi.event.EventSet;
import com.sun.jdi.event.MethodEntryEvent;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.MethodEntryRequest;
import com.sun.jdi.request.StepRequest;

public class JTIExample {

	/**
	 * @param args
	 */
	public static void main(String[] args) throws Exception {
		VirtualMachineManager vmm = Bootstrap.virtualMachineManager();
		AttachingConnector ac = vmm.attachingConnectors().get(0);

		Map<String, Connector.Argument> env = ac.defaultArguments();
		Connector.Argument port = env.get("port");
		port.setValue("8000");

		Connector.Argument hostname = env.get("hostname");
		hostname.setValue("localhost");

		VirtualMachine vm = ac.attach(env);
		EventQueue eventQueue = vm.eventQueue();
		EventRequestManager mgr = vm.eventRequestManager();

		//Wir legen die VM schlafen...
		vm.suspend();

		//Wir suchen unseren "Main-Thread"
		ThreadReference mainThread = null;
		List<ThreadReference> threads = vm.allThreads();
		for (ThreadReference thread : threads) {
			if ("main".equals(thread.name())) {
				mainThread = thread;
			}
		}

		// Hier registrieren wir einen MethodEntryRequest der dem JDI mitteilt,
		// das wir an dem Methodeneintrittsereignis interessiert sind.
		MethodEntryRequest methodEntryRequest = mgr.createMethodEntryRequest();
		methodEntryRequest.addClassFilter("*DebugDummy");
		methodEntryRequest.addThreadFilter(mainThread);
		methodEntryRequest.enable();

		//Wir lassen die VM weiterlaufen
		vm.resume();

		System.out.println("go");

		mainThread.resume();

		//wir suchen unser MethodEntryEvent im aktuellen EventSet der EventQueue
		Event event = null;
		while (true) {
			EventSet eventSet = eventQueue.remove();
			event = eventSet.eventIterator().next();
			if (event instanceof MethodEntryEvent) {
				break;
			}
		}

		MethodEntryEvent mee = (MethodEntryEvent) event;
		//Location location = mee.location();

		//Wir wollen nur über die ersten 5 Anweisungen der main Methode steppen
		//anschließend soll die Ausführung ganz normal weiter laufen...
		for (int i = 0; i < 5; i++) {
			//Wir erzeugen einen neuen StepRequest, da wir dem JDI mitteilen wollen,
			//dass wir im Einzelschritt über die nächste Anweisung gehen wollen.
			System.out.println("Try to step...");
			StepRequest stepRequest = mgr.createStepRequest(mainThread,
					StepRequest.STEP_LINE, StepRequest.STEP_OVER);
			stepRequest.addClassFilter("*DebugDummy");
			stepRequest.addCountFilter(1);
			stepRequest.enable();
			
			//Wir lassen die VM weiterlaufen
			//Diese führt nun wie von uns angefordert die nächste Anweisung aus.
			vm.resume();
			
			//Anschließend warten wir ein paar Sekunden...
			try {
				Thread.sleep(5000L);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			//Wir entfernen den Request aus der Event queue, damit wir
			//in der nächsten Iteration einen neuen Request nachschieben können.
			//Es darf nämlich immer nur ein Request per Thread gleichzeit laufen.
			mgr.deleteEventRequest(stepRequest);
		}
		
		//Wir geben die fremde VM wieder frei... wir beenden unsere Debuggingsession
		//-> Die Anwendung läuft wieder ganz normal weiter....
		vm.dispose();
	}
}

alles in allem dürfte das genug Beispiel sein um etwas entsprechendes zu deinem Problem zu bauen ... aber das wird sichelrich nicht einfach.

Hier mal noch ein Beispiel wie man das "Find References" Feature des Eclipse Debuggers mal selbst nachbauen kann:
(Läuft nur unter Mustang, weiterhin benötigt man das tools.jar im Classpath).

"Unser" Debuggger:

Java:
/**
 *
 */
package de.tutorials;

import java.util.List;
import java.util.Map;

import com.sun.jdi.Bootstrap;
import com.sun.jdi.LocalVariable;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.StackFrame;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.Value;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.VirtualMachineManager;
import com.sun.jdi.connect.AttachingConnector;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventQueue;
import com.sun.jdi.event.EventSet;
import com.sun.jdi.event.MethodEntryEvent;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.MethodEntryRequest;
import com.sun.jdi.request.StepRequest;

/**
 * @author Thomas.Darimont
 *
 */
public class Debugger {

    /**
     * @param args
     */
    public static void main(String[] args) throws Exception {

        VirtualMachine vm = connectToJVM("localhost", 8000);
        EventQueue eventQueue = vm.eventQueue();
        EventRequestManager mgr = vm.eventRequestManager();

        // Wir legen die VM schlafen...
        vm.suspend();

        // Wir suchen unseren "Main-Thread"
        ThreadReference mainThread = findMainThread(vm);

        String classFilter = "*Main";

        // Hier registrieren wir einen MethodEntryRequest der dem JDI mitteilt,
        // das wir an dem Methodeneintrittsereignis interessiert sind.
        MethodEntryRequest methodEntryRequest = mgr.createMethodEntryRequest();
        methodEntryRequest.addClassFilter(classFilter);
        methodEntryRequest.addThreadFilter(mainThread);
        methodEntryRequest.enable();

        // Wir lassen die VM weiterlaufen
        vm.resume();

        System.out.println("go");

        mainThread.resume();

        // wir suchen unser MethodEntryEvent im aktuellen EventSet der
        // EventQueue
        Event event = null;
        while (true) {
            EventSet eventSet = eventQueue.remove();
            event = eventSet.eventIterator().next();
            if (event instanceof MethodEntryEvent) {
                break;
            }
        }

        MethodEntryEvent mee = (MethodEntryEvent) event;
        // Location location = mee.location();

        // Wir wollen nur über die ersten 5 Anweisungen der main Methode steppen
        // anschließend soll die Ausführung ganz normal weiter laufen...
        for (int i = 0; i < 3; i++) {
            doSingleStep(vm, mgr, mainThread, classFilter, 1000L);
        }

        System.out.println(mainThread.frameCount());
        StackFrame stackFrame = mainThread.frame(0);

        LocalVariable localVariableClazz = stackFrame
                .visibleVariableByName("clazz");

        Value localVariableClazzValue = stackFrame.getValue(localVariableClazz);
        ObjectReference objectReference = (ObjectReference) localVariableClazzValue;
        List<ObjectReference> objectReferences = objectReference
                .referringObjects(Long.MAX_VALUE);
        for (ObjectReference o : objectReferences) {
            System.out.println(o);
        }

        vm.dispose();
    }

    /**
     * @param vm
     * @param mgr
     * @param mainThread
     * @param classFilter
     */
    private static void doSingleStep(VirtualMachine vm,
            EventRequestManager mgr, ThreadReference mainThread,
            String classFilter, long someSleepTime) {
        // Wir erzeugen einen neuen StepRequest, da wir dem JDI mitteilen
        // wollen,
        // dass wir im Einzelschritt über die nächste Anweisung gehen
        // wollen.
        System.out.println("Try to step...");
        StepRequest stepRequest = mgr.createStepRequest(mainThread,
                StepRequest.STEP_LINE, StepRequest.STEP_OVER);
        stepRequest.addClassFilter(classFilter);
        stepRequest.addCountFilter(1);
        stepRequest.enable();

        // Wir lassen die VM weiterlaufen
        // Diese führt nun wie von uns angefordert die nächste Anweisung
        // aus.
        vm.resume();

        // Anschließend warten wir ein paar Sekunden...
        try {
            Thread.sleep(someSleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Wir entfernen den Request aus der Event queue, damit wir
        // in der nächsten Iteration einen neuen Request nachschieben
        // können.
        // Es darf nämlich immer nur ein Request per Thread gleichzeit
        // laufen.
        mgr.deleteEventRequest(stepRequest);
    }

    private static VirtualMachine connectToJVM(String hostname, int port)
            throws Exception {
        VirtualMachineManager virtualMachineManager = Bootstrap
                .virtualMachineManager();
        AttachingConnector attachingConnector = (AttachingConnector) virtualMachineManager
                .attachingConnectors().get(0);

        Map<String, Connector.Argument> env = attachingConnector
                .defaultArguments();
        Connector.Argument portArgument = env.get("port");
        portArgument.setValue(port + "");

        Connector.Argument hostnameArgument = env.get("hostname");
        hostnameArgument.setValue(hostname);
        return attachingConnector.attach(env);
    }

    /**
     * @param vm
     * @param mainThread
     * @return
     */
    private static ThreadReference findMainThread(VirtualMachine vm) {
        List<ThreadReference> threads = vm.allThreads();
        for (ThreadReference thread : threads) {
            if ("main".equals(thread.name())) {
                return thread;
            }
        }
        throw new RuntimeException("Main Thread not found!");
    }

}

Unsere "Testklasse":

Java:
package de.tutorials;

/**
 * @author Thomas.Darimont
 */
public class Main {
    public static void main(String[] args) {   
        Class clazz =  String.class;
        System.out.println(clazz);
        System.out.println("FINISH");
    }
}

Ich möchte mir nun beispielsweise alle Instanzen anzeigen, welche den Inhalt meiner lokalen "clazz" Variablen referenzieren.
Aufrufen tut man das ganze dann ungefähr so:
D:\eclipse\3.3M1\eclipse\workspace\de.tutorials.training\bin>java -agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=y de.tutorials.Main

Anschließend kann man den "Debugger" schrittweise mit einem Debugger "debuggen" *g*.

Ausgabe der Testklasse:
Code:
D:\eclipse\3.3M1\eclipse\workspace\de.tutorials.training\bin>java -agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=y de.tutorials.Main
Listening for transport dt_socket at address: 8000
class java.lang.String
FINISH
Listening for transport dt_socket at address: 8000

Ausgabe des Debuggers:
Code:
go
Try to step...
Try to step...
Try to step...
1
"java.home"
"C:\Programme\Java\jdk1.6.0\jre"
"java.class.path"
"C:\Programme\Java\jre1.5.0_06\lib\ext\QTJava.zip"
".;C:\Programme\Java\jre1.5.0_06\lib\ext\QTJava.zip"
"file:/C:/Programme/Java/jdk1.6.0/jre/lib/ext/sunmscapi.jar!/"
"sun/text"
"sun.boot.class.path"
"sun/util"
"C:\Programme\Java\jdk1.6.0\jre\lib\resources.jar;C:\Programme\Java\jdk1.6.0\jre\lib\rt.jar;C:\Programme\Java\jdk1.6.0\jre\lib\sunrsasign.jar;C:\Programme\Java\jdk1.6.0\jre\lib\jsse.jar;C:\Programme\Java\jdk1.6.0\jre\lib\jce.jar;C:\Programme\Java\jdk1.6.0\jre\lib\charsets.jar;C:\Programme\Java\jdk1.6.0\jre\classes"
"C:\Programme\Java\jdk1.6.0\jre\lib\ext\localedata.jar"
"D:\eclipse\3.3M1\eclipse\workspace\de.tutorials.training\bin\."
"D:\eclipse\3.3M1\eclipse\workspace\de.tutorials.training\bin"
"com/sun/crypto/"
"sun.java.launcher"
"META-INF/JCE_RSA.RSA"
"SUN_STANDARD"
"\"
"META-INF/JCE_RSA.SF"
";"
"sun.management.compiler"
"C:\Programme\Java\jdk1.6.0\bin"
...

Hier noch ein paar weitere Artikel zum JDI:

http://www.se.uni-hannover.de/pub/File/pdfpapers/Dul2005.pdf
http://www.inodes.ch/doc/jdi.pdf
http://illegalargumentexception.blogspot.de/2009/03/java-using-jpda-to-write-debugger.html
http://www2.sys-con.com/itsg/virtualcd/java/archives/0603/loton/index.html
http://www.takipiblog.com/2013/09/24/how-to-write-your-own-java-scala-debugger/
http://www.eclipse.org/articles/Article-Debugger/how-to.html
http://docs.oracle.com/javase/7/docs/technotes/guides/jpda/

Gruß Tom
 
Hallo Tom,

erstmal danke für die schnelle und sehr gute Hilfe.
Wenn ich eine Klasse über JDI debugge, dann muss diese Klasse also in einer anderen Java-VM laufen. Und wenn ich die VM mittels des JDI anhalte, dann blockiere ich sämtliche Threads die in dieser VM laufen. Also wäre bei einer Swing-Anwendung damit dann auch der Event-Dispatch-Thread angehalten, oder?

Gibt es sonst noch Möglichkeiten eine Step-by-Step-Ausführung nur innerhalb eines Threads zu ermöglichen (am besten ohne die anderen zu blockieren) oder ist JDI die einzige Möglichkeit sowas zu erreichen?

Gruß Levin
 
Hallo!

Also wäre bei einer Swing-Anwendung damit dann auch der Event-Dispatch-Thread angehalten, oder?
Wenn du die gesamte VM schlafen legst natürlich schon ;-)...

Gibt es sonst noch Möglichkeiten eine Step-by-Step-Ausführung nur innerhalb eines Threads zu ermöglichen (am besten ohne die anderen zu blockieren) oder ist JDI die einzige Möglichkeit sowas zu erreichen?
Mittels des JDI kann man sehr genau steuern welche Threads schalfen sollen und welche nicht. Wenn du also deine "selbstdefinierten" Anweisungen über einen eigenen Thread ausführst kannst du diesen unabhänig von den anderen zur Schrittweisen Ausführung bewegen.

Wie stellst du dir das ganze eigentlich vor? Können deine Benutzer wirklich zur Laufzeit neuen Java Code eingeben der dann Compiliert und ausgeführt wird? Was passiert wenn jemand mal System.exit(0); absetzt? Hast du dazu irgendwelche Sicherheitskonzepte eingebaut (SecurityManager)? Hast du dir dazu vielleicht schon Alternativen wie die BeanShell oder Jython angeschaut?

Gruß Tom
 
Hm, dann muss ich mir das JDI mal genauer anschauen und sehen ob ich das hinbekomme mit dem Debuggen ohne gleich das ganze Programm lahm zu legen. ;)

Jap, der User schreibt Code in einer fest definierten Umgebung, er kann also nicht direkt alles machen was er möchte. Aufrufe wie System.exit(0); stehen in ner Blacklist und fliegen einfach aus dem Code raus bevor er dem Compiler übergeben wird.

BeanShell und Jython muss ich leider gestehen habe ich mir in dem Zusammenhang nicht angeschaut. Habe gar nicht an die Möglichkeit gedacht. ;)
Wobei die Sache ist, der Code den der User eingibt sollte pures Java sein, wodurch Jython ja schonmal rausfällt.
Und mit BeanShell habe ich mich nocht nicht auseinandergesetzt. Werd ich mir heute mal näher anschauen.
Danke für die Tipps!

Gruß Levin
 
Hallo Tom,

danke für den Tipp. So ähnlich haben wir es auch gelöst (nur das wir javac benutzen um den Usercode zu Bytecode zu machen und nen eigenen Preprocessor geschrieben haben, der die Userklasse nach etwas komplexeren Regeln zusammensetzt).
Jetzt standen wir aber vor dem nächsten Problem, nämlich wie wir diese über newInstance erzeugte und in einem eigenen Thread ausgeführte Klasse debuggen können, daher kam erst die Frage zum JDI :)
 

Neue Beiträge

Zurück