YouDebug script is evaluated as a Groovy script, so you can do all the normal things Groovy allows you to do, such as defining methods, calling other Java libraries, and so on. On top of that, YouDebug adds two major mechanisms:
Defining Breakpoints
Breakpoints are event callback handlers that are invoked when a certain event occurs in the target JVM. You can create breakpoints by calling breakpoint mtehods on the 'vm' object. These methods takes a closure that gets invoked when the event occurs, and they often takes additional arguments to control the nature of the breakpoint.
The following code defines a breakpoint on line 7 of the org.acme.SubStringTest.java (or whichever source file this class is defined in:)
vm.breakpoint("org.acme.SubStringTest",7) { println "I'm at SubStringTest.java line 7"; }
Some breakpoints have parameters for closures. For example, an exception breakpoint takes an object that represents the exception being thrown in the target JVM:
vm.exceptionBreakpoint("java.lang.IllegalArgumentException") { e -> e.dumpStackTrace(System.out); }
In YouDebug scripts, you often need to deal with classes in the target JVM. You can always use the string representation of the class name, but you can also use the java.lang.Class object in the YouDebug process, if it's available (that is, either those classes are the ones defined in JavaSE, or you've added your classes into the classpath of YouDebug invocation.) So the above code can be written a little more concisely as this:
vm.exceptionBreakpoint(IllegalArgumentException.class) { e -> e.dumpStackTrace(System.out); }
... or, by further using Groovy's shorthand for omitting the ".class" portion, you can do:
vm.exceptionBreakpoint(IllegalArgumentException) { e -> e.dumpStackTrace(System.out); }
Dynamically Defining/Updating Breakpoints
Breakpoint definitions are not limited to the top level. You can define new ones at any time, for example from another event handler. In the following example, an exception breakpoint is added once the execution hits SubStringTest line 7:
vm.breakpoint("org.acme.SubStringTest",7) { vm.exceptionBreakpoint(IllegalArgumentException) { e -> e.dumpStackTrace(System.out); } }
Breakpoint definitions return an object that represents the newly defined breakpoint. This object can be kept around to update the breakpoint --- such as temporarily disabling them, adding additional filters, or cancelling them. As an example, the following code activates an exception breakpoint only during the target program is executing between line 7 and 14.
vm.breakpoint("org.acme.SubStringTest",7) { def ebp = vm.exceptionBreakpoint(IllegalArgumentException) { e -> e.dumpStackTrace(System.out); } def x = vm.breakpoint("org.acme.SubStringTest",14) { // cancel the exception breakpoint and this breakpoint ebp.delete(); x.delete(); } }
Evaluating Expressions
In various places in YouDebug, proxy objects are used to represent objects in the target JVM. For example, in the above example, the variable 'e' refers to an object in the YouDebug JVM, which acts as a proxy to the exception object in the target JVM.
You can invoke methods and access fields on these objects, and they'll translate into the corresponding method invocations and field access on the target JVM. For example, the following code evaluates "e.getMessage()" on the thrown exception and prints the resutling String on the YouDebug JVM. Parameters and arguments are marshalled/unmarshalled. In this way, you can transparently access data in the target JVM.
vm.exceptionBreakpoint(IllegalArgumentException) { e -> String x = e.getMessage(); println x; }
Since the proxy object is still a Java object, some of the method calls (most notably equals and hashCode) are interpreted locally, not remotely. To invoke these methods on remote objects, instead on proxy objects, escape them by prepending it with '@', like this:
x.'@hashCode'() // calls hashCode() on the remote object x.hashCode() // calls hashCode() on the proxy object
In addition, proxy objects implement ObjectReference from JDI, so methods defined in those also need to be escaped.
Static methods/fields
Static methods/fields can be invoked/accessed on a proxy object to the remote type, much like how you invoke methods/fields on an object reference:
vm.breakpoint("Main",5) { // this will print a message in the target JVM vm.ref("java.lang.System").out.println("Hello from debugger"); // '_' is a synonym of 'ref'. Plus you can use a Class object instead of name, // hence the following equivalent code is bit shorter vm._(System).out.println("Hello from debugger"); }
Creating Instances
To create a new object instance, use the "_new" call (the first parameter can be either String or Class). Or if you already have a proxy object to type, use the "@new" syntax:
def f = vm._new(File,"/tmp/hello"); println f.exists() vm.exceptionBreakpoint(IllegalArgumentException) { e -> // create another instance of the same exception type. this is a contrived example. // 'type()' gets you a type proxy from an instance proxy. def x = e.type()."@new"(); } // or similarly, vm._(File)."@new"("/tmp/hello");
Inspecting/Manipulating Stack Frames
When a breakpoint is hit and the target JVM is suspended, you can walk the stack frames of threads and inspect variables. You do this through ThreadReference.
For example, in the following code, the debugger prints the visible variable 'i' from the current stack frame of the target JVM. A 'visible variable' includes local variables, instance fields, and then static fields in this order.
vm.breakpoint("Main",5) { println i; }
This is actually just a short-hand for delegate.i (where delegate is a reserved variable in Groovy to point to the closure delegate.) The short form is concise, but a verbose form can be useful for disambiguation, where the variable name hits other Groovy-reserved keywords, like 'owner', 'metaClass', and so on.
According to Groovy's language rules, the reserved delegate variable is only accessible from within the closure. To access the current thread from elsewhere (such as from a subroutine you define), use vm.currentThread.
Accessing 'this' object
In a breakpoint callback, the self variable refers to the this object in the target VM.
vm.breakpoint("Main",5) { println self; }
Deeper Stack Frame
In a breakpoint callback, thread refers to the JDI thread object that hit the breakpoint. This can be used to access deeper stack frames, by thread.frame(N) where N=0,1,2 and so on. thread.frame(0) is the top of the stack frame, and thread.frame(1) is the next one. In the following example, the script will print args[0] of the main method that's calling foo.
// test program class Main { public static void main(String[] args) { foo(); } public static void foo() {} } // YouDebug script vm.methodEntryBreakpoint("Main","foo") { println thread.frame(1).args[0]; }
The frame method returns StackFrame object.
Classes Without Debug Informatoin
Some classes (most notably classes in rt.jar) are compiled without local variable debug information. If you are using JavaSE 6, you can still access method parameters by their positions, like this:
vm.methodEntryBreakpoint("Main","main") { // print the first argument to the main method // the explicit use of 'delegate.' is necessary, or else it becomes just a bare literal. println delegate."@0"; }
Avoiding Name Collisions
If your variable name collides with other names visible in your Groovy script, Java keywords, etc., you can escape them by prefixing '@'. For example, if the target JVM has a variable named "for", you can reference it as delegate."@for". Explicit "delegate" prefix is needed because otherwise this becomes a string literal.