-
Notifications
You must be signed in to change notification settings - Fork 159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support step filters when stepping #106
Changes from 1 commit
07ec2c3
61dfc24
24cb8c7
6036641
00086a0
c1475c2
ce91adc
45faae5
6b1002c
586d83b
7878d81
86e3b4a
d6023f3
0656a3c
c9cc031
af7d6ee
6431ce9
fdc62f1
6ed3189
3fa4aed
9e4968b
0e4efdb
a91a16a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,12 @@ | |
import java.util.List; | ||
import java.util.Map; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
|
||
import org.apache.commons.lang3.ArrayUtils; | ||
|
||
import com.microsoft.java.debug.core.Configuration; | ||
import com.microsoft.java.debug.core.DebugEvent; | ||
import com.microsoft.java.debug.core.DebugUtility; | ||
import com.microsoft.java.debug.core.IDebugSession; | ||
|
@@ -28,15 +33,21 @@ | |
import com.microsoft.java.debug.core.protocol.Messages.Response; | ||
import com.microsoft.java.debug.core.protocol.Requests.Arguments; | ||
import com.microsoft.java.debug.core.protocol.Requests.Command; | ||
import com.microsoft.java.debug.core.protocol.Requests.DebugFilters; | ||
import com.microsoft.java.debug.core.protocol.Requests.StepArguments; | ||
import com.sun.jdi.IncompatibleThreadStateException; | ||
import com.sun.jdi.Location; | ||
import com.sun.jdi.Method; | ||
import com.sun.jdi.StackFrame; | ||
import com.sun.jdi.ThreadReference; | ||
import com.sun.jdi.event.BreakpointEvent; | ||
import com.sun.jdi.event.Event; | ||
import com.sun.jdi.event.StepEvent; | ||
import com.sun.jdi.event.ThreadDeathEvent; | ||
import com.sun.jdi.request.StepRequest; | ||
|
||
public class StepRequestHandler implements IDebugRequestHandler { | ||
private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); | ||
private Map<Long, ThreadState> threadStates; | ||
|
||
public StepRequestHandler() { | ||
|
@@ -56,63 +67,109 @@ public CompletableFuture<Response> handle(Command command, Arguments arguments, | |
} | ||
|
||
if (command == Command.CONFIGURATIONDONE) { | ||
context.getDebugSession().getEventHub().stepEvents().subscribe(debugEvent -> { | ||
handleDebugEvent(debugEvent, context.getDebugSession(), context); | ||
}); | ||
} else { | ||
long threadId = ((StepArguments) arguments).threadId; | ||
ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), threadId); | ||
if (thread != null) { | ||
setPendingStepKind(thread, command); | ||
setOriginalStackDepth(thread); | ||
setOriginalStepLocation(thread); | ||
context.getDebugSession().getEventHub().events() | ||
.filter(debugEvent -> debugEvent.event instanceof StepEvent | ||
|| debugEvent.event instanceof BreakpointEvent | ||
|| debugEvent.event instanceof ThreadDeathEvent) | ||
.subscribe(debugEvent -> { | ||
handleDebugEvent(debugEvent, context.getDebugSession(), context); | ||
}); | ||
return CompletableFuture.completedFuture(response); | ||
} | ||
|
||
long threadId = ((StepArguments) arguments).threadId; | ||
ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), threadId); | ||
if (thread != null) { | ||
try { | ||
setPendingStepKind(threadId, command); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to check the thread status before setting it? What if the thread state is already set by other requests? |
||
setOriginalStackDepth(threadId, thread.frameCount()); | ||
setOriginalStepLocation(threadId, getTopFrame(thread).location()); | ||
StepRequest stepRequest; | ||
if (command == Command.STEPIN) { | ||
DebugUtility.stepInto(thread, context.getDebugSession().getEventHub(), context.getDebugFilters().stepFilters); | ||
stepRequest = DebugUtility.stepInto(thread, context.getDebugSession().getEventHub(), context.getDebugFilters().stepFilters); | ||
} else if (command == Command.STEPOUT) { | ||
DebugUtility.stepOut(thread, context.getDebugSession().getEventHub(), context.getDebugFilters().stepFilters); | ||
stepRequest = DebugUtility.stepOut(thread, context.getDebugSession().getEventHub(), context.getDebugFilters().stepFilters); | ||
} else { | ||
DebugUtility.stepOver(thread, context.getDebugSession().getEventHub(), context.getDebugFilters().stepFilters); | ||
stepRequest = DebugUtility.stepOver(thread, context.getDebugSession().getEventHub(), context.getDebugFilters().stepFilters); | ||
} | ||
setPendingStepRequest(threadId, stepRequest); | ||
ThreadsRequestHandler.checkThreadRunningAndRecycleIds(thread, context); | ||
} catch (IncompatibleThreadStateException ex) { | ||
final String failureMessage = String.format("Failed to step because the thread %d is not suspended in the target VM.", threadId); | ||
logger.log(Level.SEVERE, failureMessage); | ||
return AdapterUtils.createAsyncErrorResponse(response, ErrorCode.STEP_FAILURE, failureMessage); | ||
} catch (IndexOutOfBoundsException ex) { | ||
final String failureMessage = String.format("Failed to step because the thread %d doesn't contain any stack frame", threadId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When would this happen? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
logger.log(Level.SEVERE, failureMessage); | ||
return AdapterUtils.createAsyncErrorResponse(response, ErrorCode.STEP_FAILURE, failureMessage); | ||
} | ||
} | ||
|
||
return CompletableFuture.completedFuture(response); | ||
} | ||
|
||
private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, IDebugAdapterContext context) { | ||
StepEvent event = (StepEvent) debugEvent.event; | ||
ThreadReference thread = event.thread(); | ||
debugEvent.shouldResume = false; | ||
if (context.getDebugFilters() != null) { | ||
if (getPendingStepKind(thread) == Command.STEPIN) { | ||
// Check if the step into operation stepped through the filtered code and stopped at an un-filtered location. | ||
if (getOriginalStackDepth(thread) + 1 < DebugUtility.getFrameCount(thread)) { | ||
// Create another stepOut request to return back where we started the step into. | ||
DebugUtility.stepOut(thread, debugSession.getEventHub(), context.getDebugFilters().stepFilters); | ||
return; | ||
} | ||
// If the ending step location is filtered, or same as the original location where the step into operation is originated, | ||
// do another step of the same kind. | ||
if (shouldFilterLocation(thread, context) || shouldDoExtraStepInto(thread)) { | ||
DebugUtility.stepInto(thread, debugSession.getEventHub(), context.getDebugFilters().stepFilters); | ||
return; | ||
Event event = debugEvent.event; | ||
|
||
// When a breakpoint occurs, abort any pending step requests from the same thread. | ||
if (event instanceof BreakpointEvent) { | ||
long threadId = ((BreakpointEvent) event).thread().uniqueID(); | ||
StepRequest pendingStepRequest = getPendingStepRequest(threadId); | ||
if (pendingStepRequest != null) { | ||
DebugUtility.deleteEventRequestSafely(debugSession.getVM().eventRequestManager(), pendingStepRequest); | ||
} | ||
setPendingStepRequest(threadId, null); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just removeThreadState()? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice. |
||
} else if (event instanceof ThreadDeathEvent) { | ||
long threadId = ((ThreadDeathEvent) event).thread().uniqueID(); | ||
removeThreadState(threadId); | ||
} else if (event instanceof StepEvent) { | ||
debugEvent.shouldResume = false; | ||
ThreadReference thread = ((StepEvent) event).thread(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The space before "event" is not necessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean the whitespace between (StepEvent) and event? checkstyle will need add a space there. |
||
long threadId = thread.uniqueID(); | ||
setPendingStepRequest(threadId, null); // clean up the pending status. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to dispose the request before setting it to null, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to dispose it because DebugUtility.stepXXX method also subscribes StepEvent and it will dispose the StepRequest via JDI. |
||
if (isDebugFiltersConfigured(context.getDebugFilters())) { | ||
try { | ||
if (getPendingStepKind(threadId) == Command.STEPIN) { | ||
// Check if the step into operation stepped through the filtered code and stopped at an un-filtered location. | ||
if (getOriginalStackDepth(threadId) + 1 < thread.frameCount()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't quite get the logic here. Let's sync more tomorrow. |
||
// Create another stepOut request to return back where we started the step into. | ||
StepRequest stepRequest = DebugUtility.stepOut(thread, debugSession.getEventHub(), context.getDebugFilters().stepFilters); | ||
setPendingStepRequest(threadId, stepRequest); | ||
return; | ||
} | ||
// If the ending step location is filtered, or same as the original location where the step into operation is originated, | ||
// do another step of the same kind. | ||
if (shouldFilterLocation(thread, context) || shouldDoExtraStepInto(thread)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't quite get the logic here. Let's sync more tomorrow. |
||
StepRequest stepRequest = DebugUtility.stepInto(thread, debugSession.getEventHub(), context.getDebugFilters().stepFilters); | ||
setPendingStepRequest(threadId, stepRequest); | ||
return; | ||
} | ||
} | ||
} catch (IncompatibleThreadStateException | IndexOutOfBoundsException ex) { | ||
// ignore. | ||
} | ||
} | ||
context.sendEvent(new Events.StoppedEvent("step", thread.uniqueID())); | ||
} | ||
context.sendEvent(new Events.StoppedEvent("step", thread.uniqueID())); | ||
} | ||
|
||
private boolean isDebugFiltersConfigured(DebugFilters filters) { | ||
if (filters == null) { | ||
return false; | ||
} | ||
return ArrayUtils.isNotEmpty(filters.stepFilters) || filters.skipConstructors | ||
|| filters.skipStaticInitializers || filters.skipSynthetics; | ||
} | ||
|
||
/** | ||
* Return true if the StepEvent's location is a Method that the user has indicated to filter. | ||
* | ||
* @throws IncompatibleThreadStateException | ||
* if the thread is not suspended in the target VM. | ||
*/ | ||
private boolean shouldFilterLocation(ThreadReference thread, IDebugAdapterContext context) { | ||
Location originalLocation = getOriginalStepLocation(thread); | ||
Location currentLocation = null; | ||
StackFrame topFrame = DebugUtility.getTopFrame(thread); | ||
if (topFrame != null) { | ||
currentLocation = topFrame.location(); | ||
} | ||
private boolean shouldFilterLocation(ThreadReference thread, IDebugAdapterContext context) throws IncompatibleThreadStateException { | ||
Location originalLocation = getOriginalStepLocation(thread.uniqueID()); | ||
Location currentLocation = getTopFrame(thread).location(); | ||
if (originalLocation == null || currentLocation == null) { | ||
return false; | ||
} | ||
|
@@ -130,16 +187,19 @@ private boolean shouldFilterMethod(Method method, IDebugAdapterContext context) | |
|
||
/** | ||
* Check if the current top stack is same as the original top stack. | ||
* | ||
* @throws IncompatibleThreadStateException | ||
* if the thread is not suspended in the target VM. | ||
*/ | ||
private boolean shouldDoExtraStepInto(ThreadReference thread) { | ||
if (getOriginalStackDepth(thread) != DebugUtility.getFrameCount(thread)) { | ||
private boolean shouldDoExtraStepInto(ThreadReference thread) throws IncompatibleThreadStateException { | ||
if (getOriginalStackDepth(thread.uniqueID()) != thread.frameCount()) { | ||
return false; | ||
} | ||
Location originalLocation = getOriginalStepLocation(thread); | ||
Location originalLocation = getOriginalStepLocation(thread.uniqueID()); | ||
if (originalLocation == null) { | ||
return false; | ||
} | ||
Location currentLocation = DebugUtility.getTopFrame(thread).location(); | ||
Location currentLocation = getTopFrame(thread).location(); | ||
Method originalMethod = originalLocation.method(); | ||
Method currentMethod = currentLocation.method(); | ||
if (!originalMethod.equals(currentMethod)) { | ||
|
@@ -151,59 +211,88 @@ private boolean shouldDoExtraStepInto(ThreadReference thread) { | |
return true; | ||
} | ||
|
||
private ThreadState getThreadState(ThreadReference thread) { | ||
if (thread == null) { | ||
return null; | ||
} | ||
long threadId = thread.uniqueID(); | ||
/** | ||
* Return the top stack frame of the target thread. | ||
* | ||
* @param thread | ||
* the target thread. | ||
* @return the top frame. | ||
* @throws IncompatibleThreadStateException | ||
* if the thread is not suspended in the target VM. | ||
* @throws IndexOutOfBoundsException | ||
* if the thread doesn't contain any stack frame. | ||
*/ | ||
private StackFrame getTopFrame(ThreadReference thread) throws IncompatibleThreadStateException { | ||
return thread.frame(0); | ||
} | ||
|
||
private ThreadState getThreadState(long threadId) { | ||
if (!threadStates.containsKey(threadId)) { | ||
threadStates.put(threadId, new ThreadState()); | ||
} | ||
return threadStates.get(threadId); | ||
} | ||
|
||
private void setPendingStepKind(ThreadReference thread, Command kind) { | ||
ThreadState state = getThreadState(thread); | ||
private void removeThreadState(long threadId) { | ||
if (threadStates.containsKey(threadId)) { | ||
threadStates.remove(threadId); | ||
} | ||
} | ||
|
||
private void setPendingStepKind(long threadId, Command kind) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kind -> Type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice. |
||
ThreadState state = getThreadState(threadId); | ||
if (state != null) { | ||
state.pendingStepKind = kind; | ||
} | ||
} | ||
|
||
private Command getPendingStepKind(ThreadReference thread) { | ||
ThreadState state = getThreadState(thread); | ||
private Command getPendingStepKind(long threadId) { | ||
ThreadState state = getThreadState(threadId); | ||
if (state == null) { | ||
return Command.UNSUPPORTED; | ||
} | ||
return state.pendingStepKind; | ||
} | ||
|
||
private void setOriginalStackDepth(ThreadReference thread) { | ||
ThreadState state = getThreadState(thread); | ||
private void setPendingStepRequest(long threadId, StepRequest stepRequest) { | ||
ThreadState state = getThreadState(threadId); | ||
if (state != null) { | ||
state.pendingStepRequest = stepRequest; | ||
} | ||
} | ||
|
||
private StepRequest getPendingStepRequest(long threadId) { | ||
ThreadState state = getThreadState(threadId); | ||
if (state != null) { | ||
state.stackDepth = DebugUtility.getFrameCount(thread); | ||
return state.pendingStepRequest; | ||
} | ||
return null; | ||
} | ||
|
||
private int getOriginalStackDepth(ThreadReference thread) { | ||
ThreadState state = getThreadState(thread); | ||
private void setOriginalStackDepth(long threadId, int depth) { | ||
ThreadState state = getThreadState(threadId); | ||
if (state != null) { | ||
state.stackDepth = depth; | ||
} | ||
} | ||
|
||
private int getOriginalStackDepth(long threadId) { | ||
ThreadState state = getThreadState(threadId); | ||
if (state == null) { | ||
return -1; | ||
} | ||
return state.stackDepth; | ||
} | ||
|
||
private void setOriginalStepLocation(ThreadReference thread) { | ||
ThreadState state = getThreadState(thread); | ||
private void setOriginalStepLocation(long threadId, Location location) { | ||
ThreadState state = getThreadState(threadId); | ||
if (state != null) { | ||
StackFrame topFrame = DebugUtility.getTopFrame(thread); | ||
if (topFrame != null) { | ||
state.stepLocation = topFrame.location(); | ||
} | ||
state.stepLocation = location; | ||
} | ||
} | ||
|
||
private Location getOriginalStepLocation(ThreadReference thread) { | ||
ThreadState state = getThreadState(thread); | ||
private Location getOriginalStepLocation(long threadId) { | ||
ThreadState state = getThreadState(threadId); | ||
if (state != null) { | ||
return state.stepLocation; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Who is responsible for removing thread state objects from the collection?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add logic to remove thread state when ThreadDeathEvent occurs.