/*******************************************************************************
* Copyright (c) 2016 Ericsson and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.eclipse.cdt.dsf.gdb.internal.service;
import java.util.Hashtable;
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.AbstractDMEvent;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.DataModelInitializedEvent;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IProcessDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IThreadDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExecutionDMContext;
import org.eclipse.cdt.dsf.debug.service.IStack;
import org.eclipse.cdt.dsf.debug.service.IStack.IFrameDMContext;
import org.eclipse.cdt.dsf.debug.service.command.ICommand;
import org.eclipse.cdt.dsf.debug.service.command.IEventListener;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.cdt.dsf.gdb.service.IGDBProcesses;
import org.eclipse.cdt.dsf.gdb.service.command.IGDBControl;
import org.eclipse.cdt.dsf.mi.service.IMIContainerDMContext;
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.output.MIConst;
import org.eclipse.cdt.dsf.mi.service.command.output.MIInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MINotifyAsyncOutput;
import org.eclipse.cdt.dsf.mi.service.command.output.MIOOBRecord;
import org.eclipse.cdt.dsf.mi.service.command.output.MIOutput;
import org.eclipse.cdt.dsf.mi.service.command.output.MIResult;
import org.eclipse.cdt.dsf.mi.service.command.output.MITuple;
import org.eclipse.cdt.dsf.mi.service.command.output.MIValue;
import org.eclipse.cdt.dsf.service.AbstractDsfService;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.osgi.framework.BundleContext;
/**
* This service keeps synchronized the CDT Debug View selection and GDB's
* internal focus.
*
* To keep the Debug View selection synchronized to CDT's selection, the service keeps
* track of what is the current GDB focus, by listening to the GDB MI notification
* "=thread-selected". When this notification is received, the service orders a change
* to CDT's Debug View selection to match, by sending an IGDBFocusChangedEvent.
*
* To keep GDB's focus synchronized to the Debug View selections, the UI listens to
* platform 'Debug Selection changed' events, and then uses this service, to order GDB
* to change focus to match the selection.
*
* Note: the mapping between the DV selection and GDB focus is not 1 to 1; there can
* be multiple debug sessions at one time, all shown in the DV. There is however a single
* effective DV selection. On the other end, each debug session has a dedicated instance
* of GDB, having its own unique focus, at any given time. Also not all DV selections map
* to a valid GDB focus.
*
* @since 5.2
*/
public class GDBFocusSynchronizer extends AbstractDsfService implements IGDBFocusSynchronizer, IEventListener
{
/** This service's opinion of what is the current GDB focus - it can be
* a process, thread or stack frame context */
private IDMContext fCurrentGDBFocus;
private IStack fStackService;
private IGDBProcesses fProcesses;
private IGDBControl fGdbcontrol;
private CommandFactory fCommandFactory;
// default initial values
private static final String THREAD_ID_DEFAULT = "1"; //$NON-NLS-1$
public GDBFocusSynchronizer(DsfSession session) {
super(session);
}
private class GDBFocusChangedEvent extends AbstractDMEvent<IDMContext>
implements IGDBFocusChangedEvent
{
public GDBFocusChangedEvent(IDMContext ctx) {
super(ctx);
}
}
@Override
protected BundleContext getBundleContext() {
return GdbPlugin.getBundleContext();
}
@Override
public void initialize(final RequestMonitor requestMonitor) {
super.initialize(new ImmediateRequestMonitor(requestMonitor) {
@Override
protected void handleSuccess() {
doInitialize(requestMonitor);
}
});
}
private void doInitialize(RequestMonitor requestMonitor) {
// obtain reference to a few needed services
fProcesses = getServicesTracker().getService(IGDBProcesses.class);
fStackService = getServicesTracker().getService(IStack.class);
fGdbcontrol = getServicesTracker().getService(IGDBControl.class);
fCommandFactory = fGdbcontrol.getCommandFactory();
register(new String[] { IGDBFocusSynchronizer.class.getName()},
new Hashtable<String, String>());
fGdbcontrol.addEventListener(this);
getSession().addServiceEventListener(this, null);
// set a sane initial value for current GDB focus.
// This value will be updated when the session has finished launching.
// See updateContexts() below.
fCurrentGDBFocus = createThreadContextFromThreadId(THREAD_ID_DEFAULT);
requestMonitor.done();
}
@Override
public void shutdown(RequestMonitor requestMonitor) {
fGdbcontrol.removeEventListener(this);
getSession().removeServiceEventListener(this);
unregister();
super.shutdown(requestMonitor);
}
@Override
public void setFocus(final IDMContext[] focus, RequestMonitor rm) {
assert focus != null;
// new Debug View thread or stack frame selection
IDMContext elem = focus[0];
// new selection is a frame?
if (elem instanceof IFrameDMContext) {
final IFrameDMContext finalFrameCtx = (IFrameDMContext)elem;
setFrameFocus(finalFrameCtx, new ImmediateRequestMonitor(rm) {
@Override
public void handleSuccess() {
// update the current focus, to match new GDB focus
fCurrentGDBFocus = finalFrameCtx;
rm.done();
}
});
}
// new selection is a thread?
else if (elem instanceof IMIExecutionDMContext) {
final IMIExecutionDMContext finalThreadCtx = (IMIExecutionDMContext)elem;
setThreadFocus(finalThreadCtx, new ImmediateRequestMonitor(rm) {
@Override
protected void handleSuccess() {
// update the current focus, to match new GDB focus
fCurrentGDBFocus = finalThreadCtx;
rm.done();
}
});
}
// new selection is a process?
else if (elem instanceof IMIContainerDMContext) {
setProcessFocus((IMIContainerDMContext)elem, rm);
}
else {
assert false;
rm.done(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID,
INVALID_HANDLE, "Invalid context to set focus to", null)); //$NON-NLS-1$);
return;
}
}
protected void setProcessFocus(IMIContainerDMContext newProc, RequestMonitor rm) {
if (newProc == null) {
rm.done(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID,
INVALID_HANDLE, "GdbFocusSynchronizer unable to resolve process context for the selected element", null)); //$NON-NLS-1$
return;
}
// There is no MI command to set the inferior. We could use the CLI 'inferior' command, but it would then
// generate a =thread-selected event, which would cause us to change the selection in the Debug view and
// select the stack frame of the first thread, or the first thread itself if it is running.
// That would prevent the user from being able to leave the selection to a process node.
// What we can do instead, is tell GDB to select the first thread of that inferior, which is what
// GDB would do anyway, but since we have an MI -thread-select command it will prevent GDB from
// issuing a =thread-selected event.
fProcesses.getProcessesBeingDebugged(newProc, new ImmediateDataRequestMonitor<IDMContext[]>(rm) {
@Override
protected void handleSuccess() {
if (getData().length > 0) {
IDMContext finalThread = getData()[0];
if (finalThread instanceof IMIExecutionDMContext) {
setThreadFocus((IMIExecutionDMContext)(finalThread), new ImmediateRequestMonitor(rm) {
@Override
protected void handleSuccess() {
// update the current focus, to match new GDB focus
fCurrentGDBFocus = finalThread;
rm.done();
}
});
return;
}
rm.done();
} else {
// If there are no threads, it probably implies the inferior is not running
// e.g., an exited process. In this case, we cannot set the thread, but it
// then becomes safe to set the inferior using the CLI command since
// there is no thread for that inferior and therefore no =thread-selected event
String miInferiorId = newProc.getGroupId();
// Remove the 'i' prefix
String cliInferiorId = miInferiorId.substring(1, miInferiorId.length());
ICommand<MIInfo> command = fCommandFactory.createCLIInferior(fGdbcontrol.getContext(), cliInferiorId);
fGdbcontrol.queueCommand(command, new ImmediateDataRequestMonitor<MIInfo> (rm) {
@Override
protected void handleSuccess() {
// update the current focus, to match new GDB focus
fCurrentGDBFocus = newProc;
rm.done();
}
});
}
}
});
}
protected void setThreadFocus(IMIExecutionDMContext newThread, RequestMonitor rm) {
if (newThread == null) {
rm.done(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID,
INVALID_HANDLE, "GdbFocusSynchronizer unable to resolve thread context for the selected element", null)); //$NON-NLS-1$
return;
}
// Create a mi-thread-select and send the command
ICommand<MIInfo> command = fCommandFactory.createMIThreadSelect(fGdbcontrol.getContext(), newThread.getThreadId());
fGdbcontrol.queueCommand(command, new ImmediateDataRequestMonitor<MIInfo> (rm));
}
protected void setFrameFocus(IFrameDMContext newFrame, RequestMonitor rm) {
if (newFrame == null) {
rm.done(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID,
INVALID_HANDLE, "GdbFocusSynchronizer unable to resolve frame context for the selected element", null)); //$NON-NLS-1$
return;
}
// We must specify the thread for which we want to set the frame in the -stack-select-frame command
IMIExecutionDMContext threadDmc = DMContexts.getAncestorOfType(newFrame, IMIExecutionDMContext.class);
if (isThreadSuspended(threadDmc)) {
// Create a mi-stack-select-frame and send the command
ICommand<MIInfo> command = fCommandFactory.createMIStackSelectFrame(threadDmc, newFrame.getLevel());
fGdbcontrol.queueCommand(command, new ImmediateDataRequestMonitor<MIInfo>(rm));
}
else {
rm.done();
}
}
private boolean isThreadSuspended(IExecutionDMContext ctx) {
assert ctx != null;
IRunControl runControl = getServicesTracker().getService(IRunControl.class);
if (runControl != null) {
return runControl.isSuspended(ctx);
}
else {
return false;
}
}
/**
* Parses gdb output for the =thread-selected notification.
* When this is detected, generate a DSF event to notify listeners
*
* example :
* =thread-selected,id="7",frame={level="0",addr="0x000000000041eab0",func="main",args=[]}
*/
@Override
public void eventReceived(Object output) {
for (MIOOBRecord oobr : ((MIOutput)output).getMIOOBRecords()) {
if (oobr instanceof MINotifyAsyncOutput) {
MINotifyAsyncOutput out = (MINotifyAsyncOutput) oobr;
String miEvent = out.getAsyncClass();
if ("thread-selected".equals(miEvent)) { //$NON-NLS-1$
// extract tid
MIResult[] results = out.getMIResults();
String tid = null;
String frameLevel = null;
for (int i = 0; i < results.length; i++) {
String var = results[i].getVariable();
MIValue val = results[i].getMIValue();
if (var.equals("frame") && val instanceof MITuple) { //$NON-NLS-1$
// dig deeper to get the frame level
MIResult[] res = ((MITuple)val).getMIResults();
for (int j = 0; j < res.length; j++) {
var = res[j].getVariable();
val = res[j].getMIValue();
if (var.equals("level")) { //$NON-NLS-1$
if (val instanceof MIConst) {
frameLevel = ((MIConst) val).getString();
}
}
}
}
else {
if (var.equals("id")) { //$NON-NLS-1$
if (val instanceof MIConst) {
tid = ((MIConst) val).getString();
}
}
}
}
// tid should never be null
assert (tid != null);
if (tid == null) {
return;
}
// update current focus
if (frameLevel == null) {
// thread running - current focus is a thread
fCurrentGDBFocus = createThreadContextFromThreadId(tid);
createAndDispatchGDBFocusChangedEvent();
}
else {
// thread suspended - current focus is a stack frame
int intFrameNum = 0;
try {
intFrameNum = Integer.parseInt(frameLevel);
}
catch (NumberFormatException e){
GdbPlugin.log(e);
}
String finalTid = tid;
fStackService.getFrames(
createThreadContextFromThreadId(finalTid),
intFrameNum, intFrameNum,
new ImmediateDataRequestMonitor<IFrameDMContext[]>() {
@Override
protected void handleCompleted() {
if (isSuccess() && getData().length > 0) {
fCurrentGDBFocus = getData()[0];
} else {
fCurrentGDBFocus = createThreadContextFromThreadId(finalTid);
}
createAndDispatchGDBFocusChangedEvent();
}
});
}
}
}
}
}
private void createAndDispatchGDBFocusChangedEvent() {
assert fCurrentGDBFocus != null;
fGdbcontrol.getSession().dispatchEvent(new GDBFocusChangedEvent(fCurrentGDBFocus),
fGdbcontrol.getProperties());
}
/**
* Creates an execution context from a thread id
*
* @param tid The thread id on which the execution context is based
*/
private IMIExecutionDMContext createThreadContextFromThreadId(String tid) {
assert tid != null;
IContainerDMContext parentContainer =
fProcesses.createContainerContextFromThreadId(fGdbcontrol.getContext(), tid);
IProcessDMContext processDmc = DMContexts.getAncestorOfType(parentContainer, IProcessDMContext.class);
IThreadDMContext threadDmc = fProcesses.createThreadContext(processDmc, tid);
return fProcesses.createExecutionContext(parentContainer, threadDmc, tid);
}
@Override
public void sessionSelected() {
// get debug view to select this session's current thread/frame
createAndDispatchGDBFocusChangedEvent();
}
@Override
public IDMContext[] getFocus() {
return new IDMContext[] { fCurrentGDBFocus };
}
@DsfServiceEventHandler
public void updateContexts(DataModelInitializedEvent event) {
// the debug session has finished launching - update the current focus
// to something sane. i.e. thread1 or thread1->frame0
IMIExecutionDMContext threadCtx = createThreadContextFromThreadId(THREAD_ID_DEFAULT);
if (!isThreadSuspended(threadCtx)) {
fCurrentGDBFocus = threadCtx;
}
else {
fStackService.getTopFrame(threadCtx, new ImmediateDataRequestMonitor<IFrameDMContext>() {
@Override
protected void handleCompleted() {
if (isSuccess()) {
fCurrentGDBFocus = getData();
} else {
fCurrentGDBFocus = threadCtx;
}
}
});
}
}
}