blob: ac548b6d92e07faaf507b78e580fcb36efc2f42a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 Ericsson 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
*******************************************************************************/
package org.eclipse.cdt.dsf.gdb.service;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import org.eclipse.cdt.debug.core.model.IChangeReverseMethodHandler.ReverseDebugMethod;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.ImmediateDataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService.ICommandControlDMContext;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.cdt.dsf.mi.service.IMICommandControl;
import org.eclipse.cdt.dsf.mi.service.IMIExecutionDMContext;
import org.eclipse.cdt.dsf.mi.service.command.CommandFactory;
import org.eclipse.cdt.dsf.mi.service.command.events.IMIDMEvent;
import org.eclipse.cdt.dsf.mi.service.command.events.MIBreakpointHitEvent;
import org.eclipse.cdt.dsf.mi.service.command.events.MIStoppedEvent;
import org.eclipse.cdt.dsf.mi.service.command.output.MIBreakpoint;
import org.eclipse.cdt.dsf.mi.service.command.output.MIFrame;
import org.eclipse.cdt.dsf.mi.service.command.output.MIInfo;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
/**
* @since 5.2
*/
public class GDBRunControl_7_12 extends GDBRunControl_7_10 {
private IMICommandControl fCommandControl;
private CommandFactory fCommandFactory;
private IGDBBackend fGDBBackEnd;
private Map<String, EnableReverseAtLocOperation> fBpIdToReverseOpMap = new HashMap<>();
public GDBRunControl_7_12(DsfSession session) {
super(session);
}
@Override
public void initialize(final RequestMonitor rm) {
super.initialize(new ImmediateRequestMonitor(rm) {
@Override
protected void handleSuccess() {
doInitialize(rm);
}
});
}
private void doInitialize(final RequestMonitor rm) {
fCommandControl = getServicesTracker().getService(IMICommandControl.class);
fGDBBackEnd = getServicesTracker().getService(IGDBBackend.class);
fCommandFactory = fCommandControl.getCommandFactory();
register(new String[] { GDBRunControl_7_12.class.getName() }, new Hashtable<String, String>());
rm.done();
}
@Override
public void suspend(IExecutionDMContext context, final RequestMonitor rm) {
canSuspend(context, new DataRequestMonitor<Boolean>(getExecutor(), rm) {
@Override
protected void handleSuccess() {
if (getData()) {
// Thread or Process
doSuspend(context, rm);
} else {
rm.done(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, INVALID_STATE,
"Context cannot be suspended.", null)); //$NON-NLS-1$
}
}
});
}
private void doSuspend(IExecutionDMContext context, final RequestMonitor rm) {
// We use the MI interrupt command when working in async mode.
if (fGDBBackEnd.useTargetAsync()) {
// Start the job before sending the interrupt command
// to make sure we don't miss the *stopped event
final MonitorSuspendJob monitorJob = new MonitorSuspendJob(0, rm);
fCommandControl.queueCommand(fCommandFactory.createMIExecInterrupt(context),
new ImmediateDataRequestMonitor<MIInfo>() {
@Override
protected void handleSuccess() {
// Nothing to do in the case of success, the monitoring job
// will take care of completing the RM once it gets the
// *stopped event.
}
@Override
protected void handleFailure() {
// In case of failure, we must cancel the monitoring job
// and indicate the failure in the rm.
monitorJob.cleanAndCancel();
rm.done(getStatus());
}
});
} else {
// Asynchronous mode is off
super.suspend(context, rm);
}
}
@Override
public boolean isTargetAcceptingCommands() {
// We shall directly return true if the async mode is ON.
if (fGDBBackEnd.useTargetAsync()) {
return true;
}
return super.isTargetAcceptingCommands();
}
/**
* @since 5.2
*/
@DsfServiceEventHandler
public void eventDispatched(ISuspendedDMEvent event) {
assert event instanceof IMIDMEvent;
if (event instanceof IMIDMEvent) {
Object evt = ((IMIDMEvent) event).getMIEvent();
if (evt instanceof MIBreakpointHitEvent) {
MIBreakpointHitEvent miEvt = (MIBreakpointHitEvent) evt;
for (EnableReverseAtLocOperation enableReverse : fBpIdToReverseOpMap.values()) {
if (breakpointHitMatchesLocation(miEvt, enableReverse)) {
// We are now stopped at the right place to initiate the recording for reverse mode
// Remove the operation from our internal map and process it
fBpIdToReverseOpMap.remove(enableReverse.fBpId);
IContainerDMContext containerContext = enableReverse.getContainerContext();
ICommandControlDMContext controlDmc = DMContexts.getAncestorOfType(containerContext,
ICommandControlDMContext.class);
ReverseDebugMethod reverseMethod = enableReverse.getReverseDebugMethod();
if (controlDmc != null && reverseMethod != null) {
enableReverseMode(controlDmc, reverseMethod, new RequestMonitor(getExecutor(), null) {
@Override
protected void handleSuccess() {
if (enableReverse.shouldTriggerContinue()) {
fCommandControl.queueCommand(
fCommandFactory.createMIExecContinue(containerContext),
new ImmediateDataRequestMonitor<MIInfo>());
}
}
});
}
// Not expecting more than one operation for the same location
break;
}
}
}
}
}
private static class EnableReverseAtLocOperation {
private final IContainerDMContext fContainerContext;
private final ReverseDebugMethod fTraceMethod;
private final String fBpId;
private final String fFileLocation;
private final String fAddrLocation;
private final boolean fTriggerContinue;
public EnableReverseAtLocOperation(IContainerDMContext containerContext, ReverseDebugMethod traceMethod,
String bpId, String fileLoc, String addr, boolean tiggerContinue) {
fContainerContext = containerContext;
fTraceMethod = traceMethod;
fBpId = bpId;
fFileLocation = fileLoc;
fAddrLocation = addr;
fTriggerContinue = tiggerContinue;
}
public IContainerDMContext getContainerContext() {
return fContainerContext;
}
public ReverseDebugMethod getReverseDebugMethod() {
return fTraceMethod;
}
public String getBreakointId() {
return fBpId;
}
public String getFileLocation() {
return fFileLocation;
}
public String getAddrLocation() {
return fAddrLocation;
}
public boolean shouldTriggerContinue() {
return fTriggerContinue;
}
@Override
public int hashCode() {
return fBpId.hashCode();
}
@Override
public boolean equals(Object other) {
if (other instanceof EnableReverseAtLocOperation) {
if (fContainerContext != null
&& fContainerContext.equals(((EnableReverseAtLocOperation) other).fContainerContext)
&& fTraceMethod != null
&& fTraceMethod.equals(((EnableReverseAtLocOperation) other).fTraceMethod) && fBpId != null
&& fBpId.equals(((EnableReverseAtLocOperation) other).fBpId) && fFileLocation != null
&& fFileLocation.equals(((EnableReverseAtLocOperation) other).fFileLocation)
&& fAddrLocation != null
&& fAddrLocation.equals(((EnableReverseAtLocOperation) other).fAddrLocation)
&& fTriggerContinue == ((EnableReverseAtLocOperation) other).fTriggerContinue) {
return true;
}
}
return false;
}
}
/**
* Changes the reverse debugging method as soon as the program is suspended at the specified breakpoint location
*
* It is recommended to use this request before the program runs or restarts in order to prevent timing issues and
* miss a suspend event
*
* Note, using the break point id to determine the stop location would be sufficient although in the case where
* multiple break points are inserted in the same location, GDB will only report one of them (e.g. GDB 7.12)
*
* Having the MIBreakpoint will give us access to the address, file and line number as well which can be used as
* alternatives to determine a matched location.
*
* This method is specially useful when using async mode with i.e. with GDB 7.12.
* Activating reverse debugging when the target is running may trigger an unresponsive GDB, this triggered the
* creation of this method
*
*/
void enableReverseModeAtBpLocation(final IContainerDMContext containerContext, final ReverseDebugMethod traceMethod,
MIBreakpoint bp, boolean triggerContinue) {
// Using an internal convention for file location i.e. file:lineNumber
String fileLoc = bp.getFile() + ":" + bp.getLine(); //$NON-NLS-1$
fBpIdToReverseOpMap.put(bp.getNumber(), new EnableReverseAtLocOperation(containerContext, traceMethod,
bp.getNumber(), fileLoc, bp.getAddress(), triggerContinue));
}
private boolean breakpointHitMatchesLocation(MIBreakpointHitEvent e, EnableReverseAtLocOperation enableReverse) {
if (enableReverse != null) {
String bpId = e.getNumber();
// Here we check three different things to see if we are stopped at the right place
// 1- The actual location in the file. But this does not work for breakpoints that
// were set on non-executable lines
// 2- The address where the breakpoint was set. But this does not work for breakpoints
// that have multiple addresses (GDB returns <MULTIPLE>.) I think that is for multi-process
// 3- The breakpoint id that was hit. But this does not work if another breakpoint
// was also set on the same line because GDB may return that breakpoint as being hit.
//
// So this works for the large majority of cases. The case that won't work is when the user
// does a runToLine to a line that is non-executable AND has another breakpoint AND
// has multiple addresses for the breakpoint. I'm mean, come on!
boolean equalFileLocation = false;
boolean equalAddrLocation = false;
boolean equalBpId = bpId.equals(enableReverse.getBreakointId());
MIFrame frame = e.getFrame();
if (frame != null) {
String fileLocation = frame.getFile() + ":" + frame.getLine(); //$NON-NLS-1$
String addrLocation = frame.getAddress();
equalFileLocation = fileLocation.equals(enableReverse.getFileLocation());
equalAddrLocation = addrLocation.equals(enableReverse.getAddrLocation());
}
if (equalFileLocation || equalAddrLocation || equalBpId) {
// We stopped at the right place
return true;
}
}
return false;
}
protected class MonitorSuspendJob extends Job {
// Bug 310274. Until we have a preference to configure timeouts,
// we need a large enough default timeout to accommodate slow
// remote sessions.
private final static int TIMEOUT_DEFAULT_VALUE = 5000;
private final RequestMonitor fRequestMonitor;
public MonitorSuspendJob(int timeout, RequestMonitor rm) {
super("Suspend monitor job."); //$NON-NLS-1$
setSystem(true);
fRequestMonitor = rm;
if (timeout <= 0) {
timeout = TIMEOUT_DEFAULT_VALUE; // default of 5 seconds
}
// Register to listen for the stopped event
getSession().addServiceEventListener(this, null);
schedule(timeout);
}
/**
* Cleanup job and cancel it.
* This method is required because super.canceling() is only called
* if the job is actually running.
*/
public boolean cleanAndCancel() {
if (getExecutor().isInExecutorThread()) {
getSession().removeServiceEventListener(this);
} else {
getExecutor().submit(new DsfRunnable() {
@Override
public void run() {
getSession().removeServiceEventListener(MonitorSuspendJob.this);
}
});
}
return cancel();
}
@DsfServiceEventHandler
public void eventDispatched(MIStoppedEvent e) {
if (e.getDMContext() != null && e.getDMContext() instanceof IMIExecutionDMContext) {
// For all-stop, this means all threads have stopped
if (cleanAndCancel()) {
fRequestMonitor.done();
}
}
}
@Override
protected IStatus run(IProgressMonitor monitor) {
// This will be called when the timeout is hit and no *stopped event was received
getExecutor().submit(new DsfRunnable() {
@Override
public void run() {
getSession().removeServiceEventListener(MonitorSuspendJob.this);
fRequestMonitor.done(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID,
IDsfStatusConstants.REQUEST_FAILED, "Suspend operation timeout.", null)); //$NON-NLS-1$
}
});
return Status.OK_STATUS;
}
}
}