blob: e3c3881840ac19dab2af8063ba46624682936ccf [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2018 Wind River Systems and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Wind River Systems - initial API and implementation
* John Dallaway - Report command execution error (Bug 539455)
*******************************************************************************/
package org.eclipse.cdt.dsf.debug.ui.viewmodel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.cdt.debug.core.CDebugUtils;
import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ThreadSafe;
import org.eclipse.cdt.dsf.datamodel.AbstractDMEvent;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.debug.service.IRunControl;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExecutionDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IResumedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.ISuspendedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.StateChangeReason;
import org.eclipse.cdt.dsf.debug.service.IRunControl.StepType;
import org.eclipse.cdt.dsf.debug.ui.IDsfDebugUIConstants;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
/**
* This class builds on top of standard run control service to provide
* functionality for step queuing and delaying. Step queuing essentially allows
* user to press and hold the step key and achieve maximum stepping speed. If
* this class is used, other service implementations, such as stack and
* expressions, can use it to avoid requesting data from debugger back end if
* another step is about to be executed.
*
* @since 1.1
*/
@ConfinedToDsfExecutor("#getExecutor()")
public final class SteppingController {
/**
* Default delay in milliseconds, that it takes the SteppingTimedOutEvent
* event to be issued after a step is started.
* @see SteppingTimedOutEvent
* @see #setStepTimeout(int)
* @see #getStepTimeout()
*/
public final static int STEPPING_TIMEOUT = 500;
/**
* The depth of the step queue. In other words, the maximum number of steps
* that are queued before the step queue manager is throwing them away.
*/
public final static int STEP_QUEUE_DEPTH = 2;
/**
* The maximum delay (in milliseconds) between steps when synchronized
* stepping is enabled. This also serves as a safeguard in the case stepping
* control participants fail to indicate completion of event processing.
*/
public final static int MAX_STEP_DELAY = 5000;
private final static boolean DEBUG = Boolean
.parseBoolean(Platform.getDebugOption("org.eclipse.cdt.dsf.ui/debug/stepping")); //$NON-NLS-1$
/**
* Indicates that the given context has been stepping for some time,
* and the UI (views and actions) may need to be updated accordingly.
*/
public static final class SteppingTimedOutEvent extends AbstractDMEvent<IExecutionDMContext> {
private SteppingTimedOutEvent(IExecutionDMContext execCtx) {
super(execCtx);
}
}
/**
* Interface for clients interested in stepping control. When a stepping
* control participant is registered with the stepping controller, it is
* expected to call
* {@link SteppingController#doneStepping(IExecutionDMContext, ISteppingControlParticipant)
* doneStepping} as soon as a "step", i.e. a suspended event has been
* processed. If synchronized stepping is enabled, further stepping is
* blocked until all stepping control participants have indicated completion
* of event processing or the maximum timeout
* {@link SteppingController#MAX_STEP_DELAY} has been reached.
*
* @see SteppingController#addSteppingControlParticipant(ISteppingControlParticipant)
* @see SteppingController#removeSteppingControlParticipant(ISteppingControlParticipant)
*/
public interface ISteppingControlParticipant {
}
private static class StepRequest {
IExecutionDMContext fContext;
StepType fStepType;
boolean inProgress = false;
StepRequest(IExecutionDMContext execCtx, StepType type) {
fContext = execCtx;
fStepType = type;
}
}
private class TimeOutRunnable extends DsfRunnable {
TimeOutRunnable(IExecutionDMContext dmc) {
fDmc = dmc;
}
private final IExecutionDMContext fDmc;
@Override
public void run() {
fTimedOutFutures.remove(fDmc);
if (getSession().isActive()) {
fTimedOutFlags.put(fDmc, Boolean.TRUE);
enableStepping(fDmc);
// Issue the stepping time-out event.
getSession().dispatchEvent(new SteppingTimedOutEvent(fDmc), null);
}
}
}
private final DsfSession fSession;
private final DsfServicesTracker fServicesTracker;
private IRunControl fRunControl;
private int fQueueDepth = STEP_QUEUE_DEPTH;
private final Map<IExecutionDMContext, List<StepRequest>> fStepQueues = new HashMap<>();
private final Map<IExecutionDMContext, Boolean> fTimedOutFlags = new HashMap<>();
private final Map<IExecutionDMContext, ScheduledFuture<?>> fTimedOutFutures = new HashMap<>();
/**
* Records the time of the last step for an execution context.
*/
private final Map<IExecutionDMContext, Long> fLastStepTimes = new HashMap<>();
/**
* Minimum step interval in milliseconds.
*/
private volatile int fMinStepInterval;
/**
* Step timeout in milliseconds.
*/
private volatile int fStepTimeout = STEPPING_TIMEOUT;
/**
* Map of execution contexts for which a step is in progress.
*/
private final Map<IExecutionDMContext, List<ISteppingControlParticipant>> fStepInProgress = new HashMap<>();
/**
* List of registered stepping control participants.
*/
private final List<ISteppingControlParticipant> fParticipants = Collections
.synchronizedList(new ArrayList<ISteppingControlParticipant>());
/**
* Property change listener. It updates the stepping control settings.
*/
private IPropertyChangeListener fPreferencesListener;
public SteppingController(DsfSession session) {
fSession = session;
fServicesTracker = new DsfServicesTracker(DsfUIPlugin.getBundleContext(), session.getId());
final IPreferenceStore store = DsfUIPlugin.getDefault().getPreferenceStore();
fPreferencesListener = new IPropertyChangeListener() {
@Override
public void propertyChange(final PropertyChangeEvent event) {
handlePropertyChanged(store, event);
}
};
store.addPropertyChangeListener(fPreferencesListener);
setMinimumStepInterval(store.getInt(IDsfDebugUIConstants.PREF_MIN_STEP_INTERVAL));
}
@ThreadSafe
public void dispose() {
try {
fSession.getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
if (fRunControl != null) {
getSession().removeServiceEventListener(SteppingController.this);
}
}
});
} catch (RejectedExecutionException e) {
// Session already gone.
}
IPreferenceStore store = DsfUIPlugin.getDefault().getPreferenceStore();
store.removePropertyChangeListener(fPreferencesListener);
fServicesTracker.dispose();
}
/**
* Configure the minimum time (in milliseconds) to wait between steps.
*
* @param interval
*/
@ThreadSafe
public void setMinimumStepInterval(int interval) {
fMinStepInterval = interval;
}
/**
* Configure the step timeout value. This controls the delay how long it takes
* to send out the {@link SteppingTimedOutEvent} after a step has been issued.
*
* @param timeout The timeout value in milliseconds (must be non-negative).
* @since 2.2
*/
@ThreadSafe
public void setStepTimeout(int timeout) {
assert timeout >= 0;
fStepTimeout = timeout;
}
/**
* @return the currently configured step timeout value
* @since 2.2
*/
@ThreadSafe
public int getStepTimeout() {
return fStepTimeout;
}
/**
* Register given stepping control participant.
* <p>
* Participants are obliged to call
* {@link #doneStepping(IExecutionDMContext, ISteppingControlParticipant)}
* when they have received and completed processing an
* {@link ISuspendedDMEvent}. If synchronized stepping is enabled, further
* stepping is disabled until all participants have indicated completion of
* processing the event.
* </p>
*
* @param participant
*/
@ThreadSafe
public void addSteppingControlParticipant(ISteppingControlParticipant participant) {
fParticipants.add(participant);
}
/**
* Unregister given stepping control participant.
*
* @param participant
*/
@ThreadSafe
public void removeSteppingControlParticipant(final ISteppingControlParticipant participant) {
fParticipants.remove(participant);
}
/**
* Indicate that participant has completed processing of the last step.
*
* @param execCtx
*/
public void doneStepping(final IExecutionDMContext execCtx, final ISteppingControlParticipant participant) {
if (DEBUG)
System.out
.println("[SteppingController] doneStepping participant=" + participant.getClass().getSimpleName()); //$NON-NLS-1$
List<ISteppingControlParticipant> participants = fStepInProgress.get(execCtx);
if (participants != null) {
participants.remove(participant);
if (participants.isEmpty()) {
doneStepping(execCtx);
}
} else {
for (IExecutionDMContext disabledCtx : fStepInProgress.keySet()) {
if (DMContexts.isAncestorOf(disabledCtx, execCtx)) {
participants = fStepInProgress.get(disabledCtx);
if (participants != null) {
participants.remove(participant);
if (participants.isEmpty()) {
doneStepping(disabledCtx);
}
}
}
}
}
}
@ThreadSafe
public DsfSession getSession() {
return fSession;
}
/**
* All access to this class should happen through this executor.
* @return the executor this class is confined to
*/
@ThreadSafe
public DsfExecutor getExecutor() {
return getSession().getExecutor();
}
private DsfServicesTracker getServicesTracker() {
return fServicesTracker;
}
private IRunControl getRunControl() {
if (fRunControl == null) {
fRunControl = getServicesTracker().getService(IRunControl.class);
getSession().addServiceEventListener(this, null);
}
return fRunControl;
}
/**
* Checks whether a step command can be queued up for given context.
*/
public void canEnqueueStep(IExecutionDMContext execCtx, StepType stepType, DataRequestMonitor<Boolean> rm) {
if (doCanEnqueueStep(execCtx, stepType)) {
rm.setData(true);
rm.done();
} else {
getRunControl().canStep(execCtx, stepType, rm);
}
}
private boolean doCanEnqueueStep(IExecutionDMContext execCtx, StepType stepType) {
return getRunControl().isStepping(execCtx) && !isSteppingTimedOut(execCtx);
}
/**
* Check whether the next step on the given execution context should be delayed
* based on the configured step delay.
*
* @param execCtx
* @return <code>true</code> if the step should be delayed
*/
private boolean shouldDelayStep(IExecutionDMContext execCtx) {
final int stepDelay = getStepDelay(execCtx);
if (DEBUG)
System.out.println("[SteppingController] shouldDelayStep delay=" + stepDelay); //$NON-NLS-1$
return stepDelay > 0;
}
/**
* Compute the delay in milliseconds before the next step for the given context may be executed.
*
* @param execCtx
* @return the number of milliseconds before the next possible step
*/
private int getStepDelay(IExecutionDMContext execCtx) {
int minStepInterval = fMinStepInterval;
if (minStepInterval > 0) {
for (IExecutionDMContext lastStepCtx : fLastStepTimes.keySet()) {
if (execCtx.equals(lastStepCtx) || DMContexts.isAncestorOf(execCtx, lastStepCtx)) {
long now = System.currentTimeMillis();
int delay = (int) (fLastStepTimes.get(lastStepCtx) + minStepInterval - now);
return Math.max(delay, 0);
}
}
}
return 0;
}
private void updateLastStepTime(IExecutionDMContext execCtx) {
long now = System.currentTimeMillis();
fLastStepTimes.put(execCtx, now);
for (IExecutionDMContext lastStepCtx : fLastStepTimes.keySet()) {
if (!execCtx.equals(lastStepCtx) && DMContexts.isAncestorOf(execCtx, lastStepCtx)) {
fLastStepTimes.put(lastStepCtx, now);
}
}
}
private long getLastStepTime(IExecutionDMContext execCtx) {
if (fLastStepTimes.containsKey(execCtx)) {
return fLastStepTimes.get(execCtx);
}
for (IExecutionDMContext lastStepCtx : fLastStepTimes.keySet()) {
if (DMContexts.isAncestorOf(execCtx, lastStepCtx)) {
return fLastStepTimes.get(lastStepCtx);
}
}
return 0;
}
/**
* Returns the number of step commands that are queued for given execution
* context.
*/
public int getPendingStepCount(IExecutionDMContext execCtx) {
List<StepRequest> stepQueue = getStepQueue(execCtx);
if (stepQueue == null)
return 0;
return stepQueue.size();
}
/**
* Adds a step command to the execution queue for given context.
* @param execCtx Execution context that should perform the step.
* @param stepType Type of step to execute.
*/
public void enqueueStep(final IExecutionDMContext execCtx, final StepType stepType) {
if (DEBUG)
System.out.println("[SteppingController] enqueueStep ctx=" + execCtx); //$NON-NLS-1$
if (!shouldDelayStep(execCtx) || doCanEnqueueStep(execCtx, stepType)) {
doEnqueueStep(execCtx, stepType);
processStepQueue(execCtx);
}
}
private void doStep(final IExecutionDMContext execCtx, final StepType stepType) {
if (DEBUG)
System.out.println("[SteppingController] doStep ctx=" + execCtx); //$NON-NLS-1$
disableStepping(execCtx);
updateLastStepTime(execCtx);
getRunControl().step(execCtx, stepType, new RequestMonitor(getExecutor(), null) {
@Override
protected void handleSuccess() {
fTimedOutFlags.remove(execCtx);
ScheduledFuture<?> currentTimeOutFuture = fTimedOutFutures.get(execCtx);
if (currentTimeOutFuture != null) {
currentTimeOutFuture.cancel(false);
}
fTimedOutFutures.put(execCtx,
getExecutor().schedule(new TimeOutRunnable(execCtx), fStepTimeout, TimeUnit.MILLISECONDS));
}
@Override
protected void handleFailure() {
// in case of a failed step - enable stepping again (bug 265267)
enableStepping(execCtx);
if (getStatus().getCode() == IDsfStatusConstants.INVALID_STATE) {
// Ignore errors. During fast stepping there can be expected race
// conditions leading to stepping errors.
return;
}
super.handleFailure();
}
@Override
protected void handleError() {
super.handleError();
CDebugUtils.error(getStatus(), SteppingController.this);
}
});
}
/**
* Enqueue the given step for later execution.
*
* @param execCtx
* @param stepType
*/
private void doEnqueueStep(final IExecutionDMContext execCtx, final StepType stepType) {
List<StepRequest> stepQueue = fStepQueues.get(execCtx);
if (stepQueue == null) {
stepQueue = new LinkedList<>();
fStepQueues.put(execCtx, stepQueue);
}
if (stepQueue.size() < fQueueDepth) {
stepQueue.add(new StepRequest(execCtx, stepType));
}
}
/**
* Returns whether the step instruction for the given context has timed out.
*/
public boolean isSteppingTimedOut(IExecutionDMContext execCtx) {
for (IExecutionDMContext timedOutCtx : fTimedOutFlags.keySet()) {
if (execCtx.equals(timedOutCtx) || DMContexts.isAncestorOf(execCtx, timedOutCtx)) {
return fTimedOutFlags.get(timedOutCtx);
}
}
return false;
}
/**
* Process next step on queue if any.
* @param execCtx
*/
private void processStepQueue(final IExecutionDMContext execCtx) {
final List<StepRequest> queue = getStepQueue(execCtx);
if (queue != null) {
final int stepDelay = getStepDelay(execCtx);
if (stepDelay > 0) {
getExecutor().schedule(new DsfRunnable() {
@Override
public void run() {
processStepQueue(execCtx);
}
}, stepDelay, TimeUnit.MILLISECONDS);
return;
}
final StepRequest request = queue.get(0);
if (DEBUG)
System.out.println("[SteppingController] processStepQueue request-in-progress=" + request.inProgress); //$NON-NLS-1$
if (!request.inProgress) {
if (isSteppingDisabled(request.fContext)) {
return;
}
request.inProgress = true;
getRunControl().canStep(request.fContext, request.fStepType,
new DataRequestMonitor<Boolean>(getExecutor(), null) {
@Override
protected void handleCompleted() {
if (isSuccess() && getData()) {
queue.remove(0);
if (queue.isEmpty())
fStepQueues.remove(request.fContext);
doStep(request.fContext, request.fStepType);
} else {
// For whatever reason we can't step anymore, so clear out
// the step queue.
fStepQueues.remove(request.fContext);
}
}
});
}
}
}
private List<StepRequest> getStepQueue(IExecutionDMContext execCtx) {
List<StepRequest> queue = fStepQueues.get(execCtx);
if (queue == null) {
for (IExecutionDMContext stepCtx : fStepQueues.keySet()) {
if (DMContexts.isAncestorOf(stepCtx, execCtx)) {
queue = fStepQueues.get(stepCtx);
break;
}
}
}
return queue;
}
/**
* Disable stepping for the given execution context.
*
* @param execCtx
*/
private void disableStepping(IExecutionDMContext execCtx) {
if (!fParticipants.isEmpty()) {
fStepInProgress.put(execCtx, new ArrayList<>(fParticipants));
}
}
/**
* Indicate that processing of the last step has completed and
* the next step can be issued.
*
* @param execCtx
*/
private void doneStepping(final IExecutionDMContext execCtx) {
if (DEBUG)
System.out.println("[SteppingController] doneStepping ctx=" + execCtx); //$NON-NLS-1$
enableStepping(execCtx);
processStepQueue(execCtx);
}
/**
* Enable stepping for the given execution context.
*
* @param execCtx
*/
public void enableStepping(final IExecutionDMContext execCtx) {
fStepInProgress.remove(execCtx);
for (IExecutionDMContext disabledCtx : fStepInProgress.keySet()) {
if (DMContexts.isAncestorOf(disabledCtx, execCtx)) {
fStepInProgress.remove(disabledCtx);
}
}
}
private boolean isSteppingDisabled(IExecutionDMContext execCtx) {
boolean disabled = fStepInProgress.containsKey(execCtx);
if (!disabled) {
for (IExecutionDMContext disabledCtx : fStepInProgress.keySet()) {
if (DMContexts.isAncestorOf(execCtx, disabledCtx)) {
disabled = true;
break;
}
}
}
if (disabled) {
long now = System.currentTimeMillis();
long lastStepTime = getLastStepTime(execCtx);
if (now - lastStepTime > MAX_STEP_DELAY) {
if (DEBUG)
System.out.println("[SteppingController] stepping control participant(s) timed out"); //$NON-NLS-1$
enableStepping(execCtx);
disabled = false;
}
}
return disabled;
}
protected void handlePropertyChanged(final IPreferenceStore store, final PropertyChangeEvent event) {
String property = event.getProperty();
if (IDsfDebugUIConstants.PREF_MIN_STEP_INTERVAL.equals(property)) {
setMinimumStepInterval(store.getInt(property));
}
}
///////////////////////////////////////////////////////////////////////////
@DsfServiceEventHandler
public void eventDispatched(final ISuspendedDMEvent e) {
// Take care of the stepping time out
boolean timedOut = false;
IExecutionDMContext dmc = e.getDMContext();
for (Iterator<Map.Entry<IExecutionDMContext, Boolean>> itr = fTimedOutFlags.entrySet().iterator(); itr
.hasNext();) {
Map.Entry<IExecutionDMContext, Boolean> entry = itr.next();
IExecutionDMContext nextDmc = entry.getKey();
if (nextDmc.equals(dmc) || DMContexts.isAncestorOf(nextDmc, dmc)) {
if (entry.getValue()) {
// after step timeout do not process queued steps
fStepQueues.remove(dmc);
timedOut = true;
}
itr.remove();
}
}
// Cancel all time-out futures related to the event context. I.e.
// - If event is on a container, all child threads are suspended and
// should not issue a stepping time-out event.
// - If event is on a thread, and resumed event was on a container then
// stepping timeout for container should be canceled as it would affect
// suspended thread.
for (Iterator<Map.Entry<IExecutionDMContext, ScheduledFuture<?>>> itr = fTimedOutFutures.entrySet()
.iterator(); itr.hasNext();) {
Map.Entry<IExecutionDMContext, ScheduledFuture<?>> entry = itr.next();
IExecutionDMContext nextDmc = entry.getKey();
if (nextDmc.equals(dmc) || DMContexts.isAncestorOf(nextDmc, dmc) || DMContexts.isAncestorOf(dmc, nextDmc)) {
entry.getValue().cancel(false);
itr.remove();
}
}
if (e.getReason() != StateChangeReason.STEP) {
// after any non-step suspend reason do not process queued steps for given context
fStepQueues.remove(dmc);
} else if (!timedOut) {
// Check if there's a step pending, if so execute it
processStepQueue(dmc);
}
}
@DsfServiceEventHandler
public void eventDispatched(final IResumedDMEvent e) {
if (e.getReason().equals(StateChangeReason.STEP)) {
final IExecutionDMContext dmc = e.getDMContext();
fTimedOutFlags.remove(dmc);
// Find any time-out futures for contexts that are children of the
// resumed context, and cancel them as they'll be replaced.
if (!fTimedOutFutures.containsKey(dmc)) {
for (Iterator<Map.Entry<IExecutionDMContext, ScheduledFuture<?>>> itr = fTimedOutFutures.entrySet()
.iterator(); itr.hasNext();) {
Map.Entry<IExecutionDMContext, ScheduledFuture<?>> entry = itr.next();
if (DMContexts.isAncestorOf(entry.getKey(), dmc)) {
entry.getValue().cancel(false);
itr.remove();
}
}
fTimedOutFutures.put(dmc,
getExecutor().schedule(new TimeOutRunnable(dmc), fStepTimeout, TimeUnit.MILLISECONDS));
}
}
}
}