Skip to content
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

Merged
merged 23 commits into from
Dec 6, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
07ec2c3
Init commit for step filter
testforstephen Nov 10, 2017
61dfc24
Add debug filter options in launch.json
testforstephen Nov 14, 2017
24cb8c7
Refactor config names
testforstephen Nov 14, 2017
6036641
fix javadoc
testforstephen Nov 14, 2017
00086a0
remove unused JDIMethod
testforstephen Nov 15, 2017
c1475c2
Merge branch 'master' into jinbo_stepfilter
testforstephen Nov 15, 2017
ce91adc
Merge branch 'master' of github.com:Microsoft/java-debug into jinbo_s…
testforstephen Nov 15, 2017
45faae5
Merge branch 'master' into jinbo_stepfilter
yaohaizh Nov 17, 2017
6b1002c
resolve merge conflicts
testforstephen Nov 22, 2017
586d83b
Merge branch 'jinbo_stepfilter' of github.com:Microsoft/java-debug in…
testforstephen Nov 22, 2017
7878d81
Remove unnecessary blank space
testforstephen Nov 22, 2017
86e3b4a
Merge branch 'master' into jinbo_stepfilter
testforstephen Nov 22, 2017
d6023f3
Refactor step handler to a standalone StepRequestHandler
testforstephen Nov 23, 2017
0656a3c
Merge branch 'jinbo_stepfilter' of github.com:Microsoft/java-debug in…
testforstephen Nov 23, 2017
c9cc031
resolve merge conflicts
testforstephen Nov 24, 2017
af7d6ee
Add logic to clean up stale thread states and step request
testforstephen Nov 27, 2017
6431ce9
fix review comments
testforstephen Nov 28, 2017
fdc62f1
Fix the concurrent issue for step request
testforstephen Nov 28, 2017
6ed3189
Use stateless StepRequestHandler to avoid concurrent issue
testforstephen Nov 29, 2017
3fa4aed
resolve merge conflicts
testforstephen Dec 4, 2017
9e4968b
Merge branch 'master' into jinbo_stepfilter
testforstephen Dec 5, 2017
0e4efdb
Merge branch 'master' into jinbo_stepfilter
akaroml Dec 6, 2017
a91a16a
Resolve review comments
testforstephen Dec 6, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@

import org.apache.commons.lang3.StringUtils;

import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.Method;
import com.sun.jdi.ObjectCollectedException;
import com.sun.jdi.StackFrame;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.VMDisconnectedException;
import com.sun.jdi.VirtualMachine;
Expand Down Expand Up @@ -424,34 +422,6 @@ public static void deleteEventRequestSafely(EventRequestManager eventManager, Li
}
}

/**
* Return the frame count of the specified thread safely.
* @param thread
* the thread reference
* @return the frame count or -1
*/
public static int getFrameCount(ThreadReference thread) {
try {
return thread.frameCount();
} catch (IncompatibleThreadStateException e) {
return -1;
}
}

/**
* Return the top stack frame of the specified thread safely.
* @param thread
* the thread reference
* @return the top stack frame
*/
public static StackFrame getTopFrame(ThreadReference thread) {
try {
return thread.frame(0);
} catch (IncompatibleThreadStateException e) {
return null;
}
}

/**
* Encode an string array to a string as the follows.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public enum ErrorCode {
EVALUATE_FAILURE(1010),
EMPTY_DEBUG_SESSION(1011),
INVALID_ENCODING(1012),
VM_TERMINATED(1013);
VM_TERMINATED(1013),
STEP_FAILURE(1014);

private int id;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Copy link
Member

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?

Copy link
Contributor Author

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.


public StepRequestHandler() {
Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first, thread.frame(i) claims it could throw IndexOutOfBoundsException. And i found a special case that could get this exception. That are system thread Attach Listener and Signal Dispatcher, when pausing them, you could find their stack frames are empty.

image

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just removeThreadState()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The space before "event" is not necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to dispose the request before setting it to null, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()) {
Copy link
Member

Choose a reason for hiding this comment

The 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)) {
Copy link
Member

Choose a reason for hiding this comment

The 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;
}
Expand All @@ -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)) {
Expand All @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind -> Type

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
}
Expand Down