|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
/* |
|
* This source code is provided to illustrate the usage of a given feature |
|
* or technique and has been deliberately simplified. Additional steps |
|
* required for a production-quality application, such as security checks, |
|
* input validation and proper error handling, might not be present in |
|
* this sample code. |
|
*/ |
|
|
|
|
|
package com.sun.tools.example.debug.bdi; |
|
|
|
import com.sun.jdi.*; |
|
import com.sun.jdi.request.*; |
|
import com.sun.jdi.connect.*; |
|
import com.sun.tools.example.debug.expr.ExpressionParser; |
|
import com.sun.tools.example.debug.expr.ParseException; |
|
|
|
import java.io.*; |
|
import java.util.*; |
|
|
|
import com.sun.tools.example.debug.event.*; |
|
|
|
import javax.swing.SwingUtilities; |
|
|
|
|
|
|
|
|
|
*/ |
|
public class ExecutionManager { |
|
|
|
private Session session; |
|
|
|
|
|
|
|
*/ |
|
int traceMode = VirtualMachine.TRACE_NONE; |
|
|
|
////////////////// Listener registration ////////////////// |
|
|
|
// Session Listeners |
|
|
|
ArrayList<SessionListener> sessionListeners = new ArrayList<SessionListener>(); |
|
|
|
public void addSessionListener(SessionListener listener) { |
|
sessionListeners.add(listener); |
|
} |
|
|
|
public void removeSessionListener(SessionListener listener) { |
|
sessionListeners.remove(listener); |
|
} |
|
|
|
// Spec Listeners |
|
|
|
ArrayList<SpecListener> specListeners = new ArrayList<SpecListener>(); |
|
|
|
public void addSpecListener(SpecListener cl) { |
|
specListeners.add(cl); |
|
} |
|
|
|
public void removeSpecListener(SpecListener cl) { |
|
specListeners.remove(cl); |
|
} |
|
|
|
// JDI Listeners |
|
|
|
ArrayList<JDIListener> jdiListeners = new ArrayList<JDIListener>(); |
|
|
|
|
|
|
|
*/ |
|
public void addJDIListener(JDIListener jl) { |
|
jdiListeners.add(jl); |
|
} |
|
|
|
|
|
|
|
*/ |
|
public void addJDIListener(int index, JDIListener jl) { |
|
jdiListeners.add(index, jl); |
|
} |
|
|
|
|
|
|
|
*/ |
|
public void removeJDIListener(JDIListener jl) { |
|
jdiListeners.remove(jl); |
|
} |
|
|
|
// App Echo Listeners |
|
|
|
private ArrayList<OutputListener> appEchoListeners = new ArrayList<OutputListener>(); |
|
|
|
public void addApplicationEchoListener(OutputListener l) { |
|
appEchoListeners.add(l); |
|
} |
|
|
|
public void removeApplicationEchoListener(OutputListener l) { |
|
appEchoListeners.remove(l); |
|
} |
|
|
|
// App Output Listeners |
|
|
|
private ArrayList<OutputListener> appOutputListeners = new ArrayList<OutputListener>(); |
|
|
|
public void addApplicationOutputListener(OutputListener l) { |
|
appOutputListeners.add(l); |
|
} |
|
|
|
public void removeApplicationOutputListener(OutputListener l) { |
|
appOutputListeners.remove(l); |
|
} |
|
|
|
// App Error Listeners |
|
|
|
private ArrayList<OutputListener> appErrorListeners = new ArrayList<OutputListener>(); |
|
|
|
public void addApplicationErrorListener(OutputListener l) { |
|
appErrorListeners.add(l); |
|
} |
|
|
|
public void removeApplicationErrorListener(OutputListener l) { |
|
appErrorListeners.remove(l); |
|
} |
|
|
|
// Diagnostic Listeners |
|
|
|
private ArrayList<OutputListener> diagnosticsListeners = new ArrayList<OutputListener>(); |
|
|
|
public void addDiagnosticsListener(OutputListener l) { |
|
diagnosticsListeners.add(l); |
|
} |
|
|
|
public void removeDiagnosticsListener(OutputListener l) { |
|
diagnosticsListeners.remove(l); |
|
} |
|
|
|
/////////// End Listener Registration ////////////// |
|
|
|
|
|
public VirtualMachine vm() { |
|
return session == null ? null : session.vm; |
|
} |
|
|
|
void ensureActiveSession() throws NoSessionException { |
|
if (session == null) { |
|
throw new NoSessionException(); |
|
} |
|
} |
|
|
|
public EventRequestManager eventRequestManager() { |
|
return vm() == null ? null : vm().eventRequestManager(); |
|
} |
|
|
|
|
|
|
|
*/ |
|
public int getTraceMode(int mode) { |
|
return traceMode; |
|
} |
|
|
|
|
|
|
|
*/ |
|
public void setTraceMode(int mode) { |
|
traceMode = mode; |
|
if (session != null) { |
|
session.setTraceMode(mode); |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
public boolean isInterrupted() { |
|
|
|
return session.interrupted; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public List<ReferenceType> allClasses() throws NoSessionException { |
|
ensureActiveSession(); |
|
return vm().allClasses(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public List<ReferenceType> findClassesByName(String name) throws NoSessionException { |
|
ensureActiveSession(); |
|
return vm().classesByName(name); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
public List<ReferenceType> findClassesMatchingPattern(String pattern) |
|
throws NoSessionException { |
|
ensureActiveSession(); |
|
List<ReferenceType> result = new ArrayList<ReferenceType>(); |
|
if (pattern.startsWith("*.")) { |
|
|
|
pattern = pattern.substring(1); |
|
for (ReferenceType type : vm().allClasses()) { |
|
if (type.name().endsWith(pattern)) { |
|
result.add(type); |
|
} |
|
} |
|
return result; |
|
} else { |
|
|
|
return vm().classesByName(pattern); |
|
} |
|
} |
|
|
|
/* |
|
* Return a list of ThreadReference objects corresponding |
|
* to the threads that are currently active in the VM. |
|
* A thread is removed from the list just before the |
|
* thread terminates. |
|
*/ |
|
|
|
public List<ThreadReference> allThreads() throws NoSessionException { |
|
ensureActiveSession(); |
|
return vm().allThreads(); |
|
} |
|
|
|
/* |
|
* Return a list of ThreadGroupReference objects corresponding |
|
* to the top-level threadgroups that are currently active in the VM. |
|
* Note that a thread group may be empty, or contain no threads as |
|
* descendents. |
|
*/ |
|
|
|
public List<ThreadGroupReference> topLevelThreadGroups() throws NoSessionException { |
|
ensureActiveSession(); |
|
return vm().topLevelThreadGroups(); |
|
} |
|
|
|
/* |
|
* Return the system threadgroup. |
|
*/ |
|
|
|
public ThreadGroupReference systemThreadGroup() |
|
throws NoSessionException { |
|
ensureActiveSession(); |
|
return vm().topLevelThreadGroups().get(0); |
|
} |
|
|
|
/* |
|
* Evaluate an expression. |
|
*/ |
|
|
|
public Value evaluate(final StackFrame f, String expr) |
|
throws ParseException, |
|
InvocationException, |
|
InvalidTypeException, |
|
ClassNotLoadedException, |
|
NoSessionException, |
|
IncompatibleThreadStateException { |
|
ExpressionParser.GetFrame frameGetter = null; |
|
ensureActiveSession(); |
|
if (f != null) { |
|
frameGetter = new ExpressionParser.GetFrame() { |
|
@Override |
|
public StackFrame get() { |
|
return f; |
|
} |
|
}; |
|
} |
|
return ExpressionParser.evaluate(expr, vm(), frameGetter); |
|
} |
|
|
|
|
|
/* |
|
* Start a new VM. |
|
*/ |
|
|
|
public void run(boolean suspended, |
|
String vmArgs, |
|
String className, |
|
String args) throws VMLaunchFailureException { |
|
|
|
endSession(); |
|
|
|
//### Set a breakpoint on 'main' method. |
|
|
|
if (suspended) { |
|
|
|
List<String> argList = new ArrayList<String>(1); |
|
argList.add("java.lang.String[]"); |
|
createMethodBreakpoint(className, "main", argList); |
|
} |
|
|
|
String cmdLine = className + " " + args; |
|
|
|
startSession(new ChildSession(this, vmArgs, cmdLine, |
|
appInput, appOutput, appError, |
|
diagnostics)); |
|
} |
|
|
|
|
|
|
|
*/ |
|
public void attach(String portName) throws VMLaunchFailureException { |
|
endSession(); |
|
|
|
//### Changes made here for connectors have broken the |
|
//### the 'Session' abstraction. The 'Session.attach()' |
|
//### method is intended to encapsulate all of the various |
|
//### ways in which session start-up can fail. (maddox 12/18/98) |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
VirtualMachineManager mgr = Bootstrap.virtualMachineManager(); |
|
AttachingConnector connector = mgr.attachingConnectors().get(0); |
|
Map<String, Connector.Argument> arguments = connector.defaultArguments(); |
|
arguments.get("port").setValue(portName); |
|
|
|
Session newSession = internalAttach(connector, arguments); |
|
if (newSession != null) { |
|
startSession(newSession); |
|
} |
|
} |
|
|
|
private Session internalAttach(AttachingConnector connector, |
|
Map<String, Connector.Argument> arguments) { |
|
try { |
|
VirtualMachine vm = connector.attach(arguments); |
|
return new Session(vm, this, diagnostics); |
|
} catch (IOException ioe) { |
|
diagnostics.putString("\n Unable to attach to target VM: " + |
|
ioe.getMessage()); |
|
} catch (IllegalConnectorArgumentsException icae) { |
|
diagnostics.putString("\n Invalid connector arguments: " + |
|
icae.getMessage()); |
|
} |
|
return null; |
|
} |
|
|
|
private Session internalListen(ListeningConnector connector, |
|
Map<String, Connector.Argument> arguments) { |
|
try { |
|
VirtualMachine vm = connector.accept(arguments); |
|
return new Session(vm, this, diagnostics); |
|
} catch (IOException ioe) { |
|
diagnostics.putString( |
|
"\n Unable to accept connection to target VM: " + |
|
ioe.getMessage()); |
|
} catch (IllegalConnectorArgumentsException icae) { |
|
diagnostics.putString("\n Invalid connector arguments: " + |
|
icae.getMessage()); |
|
} |
|
return null; |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
public boolean explictStart(Connector connector, Map<String, Connector.Argument> arguments) |
|
throws VMLaunchFailureException { |
|
Session newSession = null; |
|
|
|
endSession(); |
|
|
|
if (connector instanceof LaunchingConnector) { |
|
|
|
newSession = new ChildSession(this, (LaunchingConnector)connector, |
|
arguments, |
|
appInput, appOutput, appError, |
|
diagnostics); |
|
} else if (connector instanceof AttachingConnector) { |
|
newSession = internalAttach((AttachingConnector)connector, |
|
arguments); |
|
} else if (connector instanceof ListeningConnector) { |
|
newSession = internalListen((ListeningConnector)connector, |
|
arguments); |
|
} else { |
|
diagnostics.putString("\n Unknown connector: " + connector); |
|
} |
|
if (newSession != null) { |
|
startSession(newSession); |
|
} |
|
return newSession != null; |
|
} |
|
|
|
|
|
|
|
*/ |
|
public void detach() throws NoSessionException { |
|
ensureActiveSession(); |
|
endSession(); |
|
} |
|
|
|
private void startSession(Session s) throws VMLaunchFailureException { |
|
if (!s.attach()) { |
|
throw new VMLaunchFailureException(); |
|
} |
|
session = s; |
|
EventRequestManager em = vm().eventRequestManager(); |
|
ClassPrepareRequest classPrepareRequest = em.createClassPrepareRequest(); |
|
//### We must allow the deferred breakpoints to be resolved before |
|
//### we continue executing the class. We could optimize if there |
|
//### were no deferred breakpoints outstanding for a particular class. |
|
|
|
classPrepareRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL); |
|
classPrepareRequest.enable(); |
|
ClassUnloadRequest classUnloadRequest = em.createClassUnloadRequest(); |
|
classUnloadRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE); |
|
classUnloadRequest.enable(); |
|
ThreadStartRequest threadStartRequest = em.createThreadStartRequest(); |
|
threadStartRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE); |
|
threadStartRequest.enable(); |
|
ThreadDeathRequest threadDeathRequest = em.createThreadDeathRequest(); |
|
threadDeathRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE); |
|
threadDeathRequest.enable(); |
|
ExceptionRequest exceptionRequest = |
|
em.createExceptionRequest(null, false, true); |
|
exceptionRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL); |
|
exceptionRequest.enable(); |
|
validateThreadInfo(); |
|
session.interrupted = true; |
|
notifySessionStart(); |
|
} |
|
|
|
void endSession() { |
|
if (session != null) { |
|
session.detach(); |
|
session = null; |
|
invalidateThreadInfo(); |
|
notifySessionDeath(); |
|
} |
|
} |
|
|
|
/* |
|
* Suspend all VM activity. |
|
*/ |
|
|
|
public void interrupt() throws NoSessionException { |
|
ensureActiveSession(); |
|
vm().suspend(); |
|
|
|
validateThreadInfo(); |
|
session.interrupted = true; |
|
notifyInterrupted(); |
|
} |
|
|
|
/* |
|
* Resume interrupted VM. |
|
*/ |
|
|
|
public void go() throws NoSessionException, VMNotInterruptedException { |
|
ensureActiveSession(); |
|
invalidateThreadInfo(); |
|
session.interrupted = false; |
|
notifyContinued(); |
|
vm().resume(); |
|
} |
|
|
|
|
|
|
|
*/ |
|
void clearPreviousStep(ThreadReference thread) { |
|
|
|
|
|
|
|
*/ |
|
EventRequestManager mgr = vm().eventRequestManager(); |
|
for (StepRequest request : mgr.stepRequests()) { |
|
if (request.thread().equals(thread)) { |
|
mgr.deleteEventRequest(request); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
private void generalStep(ThreadReference thread, int size, int depth) |
|
throws NoSessionException { |
|
ensureActiveSession(); |
|
invalidateThreadInfo(); |
|
session.interrupted = false; |
|
notifyContinued(); |
|
|
|
clearPreviousStep(thread); |
|
EventRequestManager reqMgr = vm().eventRequestManager(); |
|
StepRequest request = reqMgr.createStepRequest(thread, |
|
size, depth); |
|
|
|
request.addCountFilter(1); |
|
request.enable(); |
|
vm().resume(); |
|
} |
|
|
|
public void stepIntoInstruction(ThreadReference thread) |
|
throws NoSessionException { |
|
generalStep(thread, StepRequest.STEP_MIN, StepRequest.STEP_INTO); |
|
} |
|
|
|
public void stepOverInstruction(ThreadReference thread) |
|
throws NoSessionException { |
|
generalStep(thread, StepRequest.STEP_MIN, StepRequest.STEP_OVER); |
|
} |
|
|
|
public void stepIntoLine(ThreadReference thread) |
|
throws NoSessionException, |
|
AbsentInformationException { |
|
generalStep(thread, StepRequest.STEP_LINE, StepRequest.STEP_INTO); |
|
} |
|
|
|
public void stepOverLine(ThreadReference thread) |
|
throws NoSessionException, |
|
AbsentInformationException { |
|
generalStep(thread, StepRequest.STEP_LINE, StepRequest.STEP_OVER); |
|
} |
|
|
|
public void stepOut(ThreadReference thread) |
|
throws NoSessionException { |
|
generalStep(thread, StepRequest.STEP_MIN, StepRequest.STEP_OUT); |
|
} |
|
|
|
/* |
|
* Thread control. |
|
*/ |
|
|
|
public void suspendThread(ThreadReference thread) throws NoSessionException { |
|
ensureActiveSession(); |
|
thread.suspend(); |
|
} |
|
|
|
public void resumeThread(ThreadReference thread) throws NoSessionException { |
|
ensureActiveSession(); |
|
thread.resume(); |
|
} |
|
|
|
public void stopThread(ThreadReference thread) throws NoSessionException { |
|
ensureActiveSession(); |
|
//### Need an exception now. Which one to use? |
|
//thread.stop(); |
|
} |
|
|
|
/* |
|
* ThreadInfo objects -- Allow query of thread status and stack. |
|
*/ |
|
|
|
private List<ThreadInfo> threadInfoList = new LinkedList<ThreadInfo>(); |
|
|
|
private HashMap<ThreadReference, ThreadInfo> threadInfoMap = new HashMap<ThreadReference, ThreadInfo>(); |
|
|
|
public ThreadInfo threadInfo(ThreadReference thread) { |
|
if (session == null || thread == null) { |
|
return null; |
|
} |
|
ThreadInfo info = threadInfoMap.get(thread); |
|
if (info == null) { |
|
//### Should not hardcode initial frame count and prefetch here! |
|
|
|
info = new ThreadInfo(thread); |
|
if (session.interrupted) { |
|
info.validate(); |
|
} |
|
threadInfoList.add(info); |
|
threadInfoMap.put(thread, info); |
|
} |
|
return info; |
|
} |
|
|
|
void validateThreadInfo() { |
|
session.interrupted = true; |
|
for (ThreadInfo threadInfo : threadInfoList) { |
|
threadInfo.validate(); |
|
} |
|
} |
|
|
|
private void invalidateThreadInfo() { |
|
if (session != null) { |
|
session.interrupted = false; |
|
for (ThreadInfo threadInfo : threadInfoList) { |
|
threadInfo.invalidate(); |
|
} |
|
} |
|
} |
|
|
|
void removeThreadInfo(ThreadReference thread) { |
|
ThreadInfo info = threadInfoMap.get(thread); |
|
if (info != null) { |
|
info.invalidate(); |
|
threadInfoMap.remove(thread); |
|
threadInfoList.remove(info); |
|
} |
|
} |
|
|
|
/* |
|
* Listen for Session control events. |
|
*/ |
|
|
|
private void notifyInterrupted() { |
|
ArrayList<SessionListener> l = new ArrayList<SessionListener>(sessionListeners); |
|
EventObject evt = new EventObject(this); |
|
for (int i = 0; i < l.size(); i++) { |
|
l.get(i).sessionInterrupt(evt); |
|
} |
|
} |
|
|
|
private void notifyContinued() { |
|
ArrayList<SessionListener> l = new ArrayList<SessionListener>(sessionListeners); |
|
EventObject evt = new EventObject(this); |
|
for (int i = 0; i < l.size(); i++) { |
|
l.get(i).sessionContinue(evt); |
|
} |
|
} |
|
|
|
private void notifySessionStart() { |
|
ArrayList<SessionListener> l = new ArrayList<SessionListener>(sessionListeners); |
|
EventObject evt = new EventObject(this); |
|
for (int i = 0; i < l.size(); i++) { |
|
l.get(i).sessionStart(evt); |
|
} |
|
} |
|
|
|
private void notifySessionDeath() { |
|
/*** noop for now |
|
ArrayList<SessionListener> l = new ArrayList<SessionListener>(sessionListeners); |
|
EventObject evt = new EventObject(this); |
|
for (int i = 0; i < l.size(); i++) { |
|
((SessionListener)l.get(i)).sessionDeath(evt); |
|
} |
|
****/ |
|
} |
|
|
|
/* |
|
* Listen for input and output requests from the application |
|
* being debugged. These are generated only when the debuggee |
|
* is spawned as a child of the debugger. |
|
*/ |
|
|
|
private Object inputLock = new Object(); |
|
private LinkedList<String> inputBuffer = new LinkedList<String>(); |
|
|
|
private void resetInputBuffer() { |
|
synchronized (inputLock) { |
|
inputBuffer = new LinkedList<String>(); |
|
} |
|
} |
|
|
|
public void sendLineToApplication(String line) { |
|
synchronized (inputLock) { |
|
inputBuffer.addFirst(line); |
|
inputLock.notifyAll(); |
|
} |
|
} |
|
|
|
private InputListener appInput = new InputListener() { |
|
@Override |
|
public String getLine() { |
|
|
|
String line = null; |
|
while (line == null) { |
|
synchronized (inputLock) { |
|
try { |
|
while (inputBuffer.size() < 1) { |
|
inputLock.wait(); |
|
} |
|
line = inputBuffer.removeLast(); |
|
} catch (InterruptedException e) {} |
|
} |
|
} |
|
// We must not be holding inputLock here, as the listener |
|
// that we call to echo a line might call us re-entrantly |
|
// to provide another line of input. |
|
|
|
final String input = line; |
|
SwingUtilities.invokeLater(new Runnable() { |
|
@Override |
|
public void run() { |
|
echoInputLine(input); |
|
} |
|
}); |
|
return line; |
|
} |
|
}; |
|
|
|
private static String newline = System.getProperty("line.separator"); |
|
|
|
private void echoInputLine(String line) { |
|
ArrayList<OutputListener> l = new ArrayList<OutputListener>(appEchoListeners); |
|
for (int i = 0; i < l.size(); i++) { |
|
OutputListener ol = l.get(i); |
|
ol.putString(line); |
|
ol.putString(newline); |
|
} |
|
} |
|
|
|
private OutputListener appOutput = new OutputListener() { |
|
@Override |
|
public void putString(String string) { |
|
ArrayList<OutputListener> l = new ArrayList<OutputListener>(appEchoListeners); |
|
for (int i = 0; i < l.size(); i++) { |
|
l.get(i).putString(string); |
|
} |
|
} |
|
}; |
|
|
|
private OutputListener appError = new OutputListener() { |
|
@Override |
|
public void putString(String string) { |
|
ArrayList<OutputListener> l = new ArrayList<OutputListener>(appEchoListeners); |
|
for (int i = 0; i < l.size(); i++) { |
|
l.get(i).putString(string); |
|
} |
|
} |
|
}; |
|
|
|
private OutputListener diagnostics = new OutputListener() { |
|
@Override |
|
public void putString(String string) { |
|
ArrayList<OutputListener> l = new ArrayList<OutputListener>(diagnosticsListeners); |
|
for (int i = 0; i < l.size(); i++) { |
|
l.get(i).putString(string); |
|
} |
|
} |
|
}; |
|
|
|
///////////// Spec Request Creation/Deletion/Query /////////// |
|
|
|
private EventRequestSpecList specList = new EventRequestSpecList(this); |
|
|
|
public BreakpointSpec |
|
createSourceLineBreakpoint(String sourceName, int line) { |
|
return specList.createSourceLineBreakpoint(sourceName, line); |
|
} |
|
|
|
public BreakpointSpec |
|
createClassLineBreakpoint(String classPattern, int line) { |
|
return specList.createClassLineBreakpoint(classPattern, line); |
|
} |
|
|
|
public BreakpointSpec |
|
createMethodBreakpoint(String classPattern, |
|
String methodId, List<String> methodArgs) { |
|
return specList.createMethodBreakpoint(classPattern, |
|
methodId, methodArgs); |
|
} |
|
|
|
public ExceptionSpec |
|
createExceptionIntercept(String classPattern, |
|
boolean notifyCaught, |
|
boolean notifyUncaught) { |
|
return specList.createExceptionIntercept(classPattern, |
|
notifyCaught, |
|
notifyUncaught); |
|
} |
|
|
|
public AccessWatchpointSpec |
|
createAccessWatchpoint(String classPattern, String fieldId) { |
|
return specList.createAccessWatchpoint(classPattern, fieldId); |
|
} |
|
|
|
public ModificationWatchpointSpec |
|
createModificationWatchpoint(String classPattern, String fieldId) { |
|
return specList.createModificationWatchpoint(classPattern, |
|
fieldId); |
|
} |
|
|
|
public void delete(EventRequestSpec spec) { |
|
specList.delete(spec); |
|
} |
|
|
|
void resolve(ReferenceType refType) { |
|
specList.resolve(refType); |
|
} |
|
|
|
public void install(EventRequestSpec spec) { |
|
specList.install(spec, vm()); |
|
} |
|
|
|
public List<EventRequestSpec> eventRequestSpecs() { |
|
return specList.eventRequestSpecs(); |
|
} |
|
} |