| /******************************************************************************* |
| * 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; |
| } |
| } |
| } |