blob: 8db4f3b9bf343a51812b3967f4745c290a41ce5e [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
* Ericsson - Modified for handling of multiple stacks and threads
* Nokia - create and use backend service.
* Onur Akdemir (TUBITAK BILGEM-ITI) - Multi-process debugging (Bug 237306)
* Marc Khouzam (Ericsson) - New method to properly created ErrorThread (Bug 350837)
* Jason Litton (Sage Electronic Engineering, LLC) - Use Dynamic Tracing option (Bug 379169)
* Jonah Graham (Kichwa Coders) - Bug 317173 - cleanup warnings
* John Dallaway - Decode line breaks in status message (Bug 539455)
*******************************************************************************/
package org.eclipse.cdt.dsf.mi.service.command;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
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.IDMContext;
import org.eclipse.cdt.dsf.debug.service.ICachingService;
import org.eclipse.cdt.dsf.debug.service.IRunControl;
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.ICommandControlService;
import org.eclipse.cdt.dsf.debug.service.command.ICommandListener;
import org.eclipse.cdt.dsf.debug.service.command.ICommandResult;
import org.eclipse.cdt.dsf.debug.service.command.ICommandToken;
import org.eclipse.cdt.dsf.debug.service.command.IEventListener;
import org.eclipse.cdt.dsf.gdb.IGdbDebugPreferenceConstants;
import org.eclipse.cdt.dsf.gdb.internal.GdbDebugOptions;
import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin;
import org.eclipse.cdt.dsf.mi.service.IMICommandControl;
import org.eclipse.cdt.dsf.mi.service.IMIContainerDMContext;
import org.eclipse.cdt.dsf.mi.service.IMIExecutionDMContext;
import org.eclipse.cdt.dsf.mi.service.command.commands.MICommand;
import org.eclipse.cdt.dsf.mi.service.command.commands.RawCommand;
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.MIList;
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.MIParser;
import org.eclipse.cdt.dsf.mi.service.command.output.MIResult;
import org.eclipse.cdt.dsf.mi.service.command.output.MIResultRecord;
import org.eclipse.cdt.dsf.mi.service.command.output.MIStreamRecord;
import org.eclipse.cdt.dsf.mi.service.command.output.MIValue;
import org.eclipse.cdt.dsf.service.AbstractDsfService;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.osgi.util.NLS;
import com.ibm.icu.text.MessageFormat;
/**
* Base implementation of an MI control service. It provides basic handling
* of input/output channels, and processing of the commands.
* <p>
* Extending classes need to implement the initialize() and shutdown() methods.
*/
public abstract class AbstractMIControl extends AbstractDsfService implements IMICommandControl {
private static final String MI_TRACE_IDENTIFIER = "[MI]"; //$NON-NLS-1$
private static final int NUMBER_CONCURRENT_COMMANDS = 3;
private static final int DEVELOPMENT_TRACE_LIMIT_CHARS = 5000;
/*
* Thread control variables for the transmit and receive threads.
*/
private TxThread fTxThread;
private RxThread fRxThread;
private ErrorThread fErrorThread;
// MI did not always support the --thread/--frame options
// This boolean is used to know if we should use -thread-select and -stack-select-frame instead
private boolean fUseThreadAndFrameOptions;
// currentStackLevel and currentThreadId are only necessary when
// we must use -thread-select and -stack-select-frame
private int fCurrentStackLevel = -1;
private String fCurrentThreadId = null;
// boolean for --thread-group option which helps to handle multiple inferior behavior.
// Since GDB.7.1
private boolean fUseThreadGroupOption;
private final BlockingQueue<CommandHandle> fTxCommands = new LinkedBlockingQueue<>();
private final Map<Integer, CommandHandle> fRxCommands = Collections
.synchronizedMap(new HashMap<Integer, CommandHandle>());
/**
* Handle that's inserted into the TX commands queue to signal
* that the TX thread should shut down.
*/
private final CommandHandle fTerminatorHandle = new CommandHandle(null, null);
/*
* Various listener control variables used to keep track of listeners who want to monitor
* what the control object is doing.
*/
private final List<ICommandListener> fCommandProcessors = new ArrayList<>();
private final List<IEventListener> fEventProcessors = new ArrayList<>();
/**
* Current command which have not been handed off to the backend yet.
*/
private final List<CommandHandle> fCommandQueue = new ArrayList<>();
/**
* Flag indicating that the command control has stopped processing commands.
*/
private boolean fStoppedCommandProcessing = false;
/**
* An output stream that MI communication should be output to.
* It serves for debugging. Can be <code>null</code> to disable tracing.
*/
private OutputStream fTracingStream = null;
private CommandFactory fCommandFactory;
/**
* Event indicating that the back end process has started.
*/
private static class RefreshAllDMEvent extends AbstractDMEvent<ICommandControlDMContext>
implements ICommandControlRefreshAllDMEvent {
public RefreshAllDMEvent(ICommandControlDMContext context) {
super(context);
}
}
public AbstractMIControl(DsfSession session) {
this(session, false, false, new CommandFactory());
}
/**
* @since 3.0
*/
public AbstractMIControl(DsfSession session, boolean useThreadAndFrameOptions, CommandFactory factory) {
this(session, false, useThreadAndFrameOptions, factory);
}
/**
* @since 4.0
*/
public AbstractMIControl(DsfSession session, boolean useThreadGroupOption, boolean useThreadAndFrameOptions,
CommandFactory factory) {
super(session);
// If we use the --thread-group option, we should automatically use the --thread/--frame option
// since the --thread-group was added to GDB later than the --thread/--frame option
assert useThreadGroupOption ? useThreadAndFrameOptions : true;
fUseThreadGroupOption = useThreadGroupOption;
fUseThreadAndFrameOptions = useThreadAndFrameOptions;
if (fUseThreadGroupOption) {
// If we use --thread-group option, we should automatically use the --thread option
fUseThreadAndFrameOptions = true;
}
fCommandFactory = factory;
}
/**
* Set the tracing stream for the MI communication. If this method is never
* called, tracing will be off, by default.
*
* @param tracingStream The stream to use. Can be <code>null</code>
* to disable tracing.
* @since 2.0
*/
protected synchronized void setMITracingStream(OutputStream tracingStream) {
fTracingStream = tracingStream;
}
/**
* Returns the MI tracing stream.
*/
private synchronized OutputStream getMITracingStream() {
return fTracingStream;
}
/**
* @since 3.0
*/
protected void setUseThreadAndFrameOptions(boolean shouldUse) {
fUseThreadAndFrameOptions = shouldUse;
}
/**
* @since 4.0
*/
protected void setUseThreadGroupOptions(boolean shouldUse) {
fUseThreadGroupOption = shouldUse;
if (shouldUse) {
fUseThreadAndFrameOptions = true;
}
}
/**
* @since 3.0
*/
@Override
public CommandFactory getCommandFactory() {
return fCommandFactory;
}
/**
* Starts the threads that process the debugger input/output channels.
* To be invoked by the initialization routine of the extending class.
*
* This version of the method will not start a thread for the error stream.
*
* @param inStream
* @param outStream
*/
protected void startCommandProcessing(InputStream inStream, OutputStream outStream) {
startCommandProcessing(inStream, outStream, null);
}
/**
* Starts the threads that process the debugger input/output/error channels.
* To be invoked by the initialization routine of the extending class.
*
*
* @param inStream
* @param outStream
* @param errorStream
* @since 4.1
*/
protected void startCommandProcessing(InputStream inStream, OutputStream outStream, InputStream errorStream) {
fTxThread = new TxThread(outStream);
fRxThread = new RxThread(inStream);
if (errorStream != null) {
fErrorThread = new ErrorThread(errorStream);
}
fTxThread.start();
fRxThread.start();
if (fErrorThread != null) {
fErrorThread.start();
}
}
/**
* Stops the threads that process the debugger input/output channels, and notifies the
* results of the outstanding commands. To be invoked by the shutdown routine of the
* extending class.
*
* @param inStream
* @param outStream
*/
private Status genStatus(String str) {
return new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, INVALID_STATE, str, null);
}
protected void stopCommandProcessing() {
// Guard against calling this multiple times (e.g. as a result of a
// user request and an event from the back end).
if (fStoppedCommandProcessing)
return;
fStoppedCommandProcessing = true;
/*
* First go through the commands which have been queueud and not yet sent to the backend.
*/
for (CommandHandle commandHandle : fCommandQueue) {
if (commandHandle.getRequestMonitor() == null)
continue;
commandHandle.getRequestMonitor().setStatus(genStatus("Connection is shut down")); //$NON-NLS-1$
commandHandle.getRequestMonitor().done();
}
fCommandQueue.clear();
/*
* Now go through the commands which are outstanding in that they have been sent to the backend.
*/
cancelRxCommands();
/*
* Now handle any requests which have not been transmitted, but weconsider them handed off.
*/
List<CommandHandle> txCommands = new ArrayList<>();
fTxCommands.drainTo(txCommands);
for (CommandHandle commandHandle : txCommands) {
if (commandHandle.getRequestMonitor() == null)
continue;
commandHandle.getRequestMonitor().setStatus(genStatus("Connection is shut down")); //$NON-NLS-1$
commandHandle.getRequestMonitor().done();
}
// Queue a null value to tell the send thread to shut down.
fTxCommands.add(fTerminatorHandle);
}
private void cancelRxCommands() {
Map<Integer, CommandHandle> rxCommandsCopy;
synchronized (fRxCommands) {
rxCommandsCopy = new LinkedHashMap<>(fRxCommands);
fRxCommands.clear();
}
for (CommandHandle commandHandle : rxCommandsCopy.values()) {
if (commandHandle.getRequestMonitor() == null)
continue;
commandHandle.getRequestMonitor().setStatus(genStatus("Connection is shut down")); //$NON-NLS-1$
commandHandle.getRequestMonitor().done();
}
}
/**
* Queues the given MI command to be sent to the debugger back end.
*
* @param command Command to be executed. This parameter must be an
* instance of DsfMICommand, otherwise a ClassCastException will be
* thrown.
* @param rm Request completion monitor
*
* @see org.eclipse.cdt.dsf.debug.service.command.ICommandControl#addCommand(org.eclipse.cdt.dsf.debug.service.command.ICommand, org.eclipse.cdt.dsf.concurrent.RequestMonitor)
*/
@Override
public <V extends ICommandResult> ICommandToken queueCommand(final ICommand<V> command, DataRequestMonitor<V> rm) {
// Cast the command to MI Command type. This will cause a cast exception to be
// thrown if the client did not give an MI command as an argument.
@SuppressWarnings("unchecked")
MICommand<MIInfo> miCommand = (MICommand<MIInfo>) command;
// Cast the return token to match the result type of MI Command. This is checking
// against an erased type so it should never throw any exceptions.
@SuppressWarnings("unchecked")
DataRequestMonitor<MIInfo> miDone = (DataRequestMonitor<MIInfo>) rm;
final CommandHandle handle = new CommandHandle(miCommand, miDone);
// If the command control stopped processing commands, just return an error immediately.
if (fStoppedCommandProcessing) {
rm.setStatus(genStatus("Connection is shut down")); //$NON-NLS-1$
rm.done();
} else {
/*
* We only allow three outstanding commands to be on the wire to the backend
* at any one time. This allows for coalescing as well as canceling
* existing commands on a state change. So we add it to the waiting list and let
* the user know they can now work with this item if need be.
*/
fCommandQueue.add(handle);
processCommandQueued(handle);
if (fRxCommands.size() < NUMBER_CONCURRENT_COMMANDS) {
// In a separate dispatch cycle. This allows command listeners
// to respond to the command queued event.
getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
processNextQueuedCommand();
}
});
}
}
return handle;
}
private void processNextQueuedCommand() {
if (!fCommandQueue.isEmpty()) {
final CommandHandle handle = fCommandQueue.remove(0);
if (handle != null) {
processCommandSent(handle);
// Older debuggers didn't support the --thread/--frame options
// Also, not all commands support those options (e.g., CLI commands)
if (!fUseThreadAndFrameOptions || !handle.getCommand().supportsThreadAndFrameOptions()) {
// Without the --thread/--frame, we need to send the proper
// -thread-select and -stack-frame-select before sending the command
final IDMContext targetContext = handle.fCommand.getContext();
final String targetThread = handle.getThreadId();
final int targetFrame = handle.getStackFrameId();
// The thread-select and frame-select make sense only if the thread is stopped.
IRunControl runControl = getServicesTracker().getService(IRunControl.class);
IMIExecutionDMContext execDmc = DMContexts.getAncestorOfType(targetContext,
IMIExecutionDMContext.class);
if (runControl != null && execDmc != null && runControl.isSuspended(execDmc)) {
// Before the command is sent, Check the Thread Id and send it to
// the queue only if the id has been changed. Also, don't send a threadId of 0,
// because that id is only used internally for single-threaded programs
if (targetThread != null && !targetThread.equals("0") //$NON-NLS-1$
&& !targetThread.equals(fCurrentThreadId)) {
fCurrentThreadId = targetThread;
resetCurrentStackLevel();
CommandHandle cmdHandle = new CommandHandle((MICommand<MIInfo>) getCommandFactory()
.createMIThreadSelect(targetContext, targetThread), null);
cmdHandle.generateTokenId();
fTxCommands.add(cmdHandle);
}
// Before the command is sent, Check the Stack level and send it to
// the queue only if the level has been changed.
if (targetFrame >= 0 && targetFrame != fCurrentStackLevel) {
fCurrentStackLevel = targetFrame;
CommandHandle cmdHandle = new CommandHandle((MICommand<MIInfo>) getCommandFactory()
.createMIStackSelectFrame(targetContext, targetFrame), null);
cmdHandle.generateTokenId();
fTxCommands.add(cmdHandle);
}
}
}
if (!(handle.getCommand() instanceof RawCommand)) {
// Only generate a token id if the command is not a RawCommand
// RawCommands are sent to GDB without an answer expected, so we don't
// need a token id. In fact, GDB will fail if we send one in this case.
handle.generateTokenId();
}
fTxCommands.add(handle);
}
}
}
/*
* This is the command which allows the user to retract a previously issued command. The
* state of the command is that it is in the waiting queue and has not yet been handed
* to the backend yet.
*
* (non-Javadoc)
* @see org.eclipse.cdt.dsf.mi.service.command.IDebuggerControl#removeCommand(org.eclipse.cdt.dsf.mi.service.command.commands.ICommand)
*/
@Override
public void removeCommand(ICommandToken token) {
synchronized (fCommandQueue) {
for (CommandHandle handle : fCommandQueue) {
if (handle.equals(token)) {
fCommandQueue.remove(handle);
final CommandHandle finalHandle = handle;
getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
processCommandRemoved(finalHandle);
}
});
break;
}
}
}
}
/*
* Allows a user ( typically a cache manager ) to sign up a listener to monitor command queue
* activity.
*
* (non-Javadoc)
* @see org.eclipse.cdt.dsf.mi.service.command.IDebuggerControl#addCommandListener(org.eclipse.cdt.dsf.mi.service.command.IDebuggerControl.ICommandListener)
*/
@Override
public void addCommandListener(ICommandListener processor) {
fCommandProcessors.add(processor);
}
/*
* Allows a user ( typically a cache manager ) to remove a monitoring listener.
* (non-Javadoc)
* @see org.eclipse.cdt.dsf.mi.service.command.IDebuggerControl#removeCommandListener(org.eclipse.cdt.dsf.mi.service.command.IDebuggerControl.ICommandListener)
*/
@Override
public void removeCommandListener(ICommandListener processor) {
fCommandProcessors.remove(processor);
}
/*
* Allows a user to sign up to a listener which handles out of band messages ( events ).
*
* (non-Javadoc)
* @see org.eclipse.cdt.dsf.mi.service.command.IDebuggerControl#addEventListener(org.eclipse.cdt.dsf.mi.service.command.IDebuggerControl.IEventListener)
*/
@Override
public void addEventListener(IEventListener processor) {
fEventProcessors.add(processor);
}
/*
* Allows a user to remove a event monitoring listener.
*
* (non-Javadoc)
* @see org.eclipse.cdt.dsf.mi.service.command.IDebuggerControl#removeEventListener(org.eclipse.cdt.dsf.mi.service.command.IDebuggerControl.IEventListener)
*/
@Override
public void removeEventListener(IEventListener processor) {
fEventProcessors.remove(processor);
}
/** @deprecated Replaced with {@link ICommandControlService#getContext()} */
@Deprecated
abstract public MIControlDMContext getControlDMContext();
/**
* @since 1.1
*/
@Override
public boolean isActive() {
return !fStoppedCommandProcessing;
}
/*
* These are the service routines which perform the various callouts back to the listeners.
*/
private void processCommandQueued(CommandHandle commandHandle) {
for (ICommandListener processor : fCommandProcessors) {
processor.commandQueued(commandHandle);
}
}
private void processCommandRemoved(CommandHandle commandHandle) {
for (ICommandListener processor : fCommandProcessors) {
processor.commandRemoved(commandHandle);
}
}
private void processCommandSent(CommandHandle commandHandle) {
for (ICommandListener processor : fCommandProcessors) {
processor.commandSent(commandHandle);
}
}
private void processCommandDone(CommandHandle commandHandle, ICommandResult result) {
/*
* Tell the listeners we have completed this one.
*/
for (ICommandListener processor : fCommandProcessors) {
processor.commandDone(commandHandle, result);
}
}
private void processEvent(MIOutput output) {
for (IEventListener processor : fEventProcessors) {
processor.eventReceived(output);
}
}
/*
* A global counter for all command, the token will be use to identify uniquely a command.
* Unless the value wraps around which is unlikely.
*/
private int fTokenIdCounter = 0;
private int getNewTokenId() {
int count = ++fTokenIdCounter;
// If we ever wrap around.
if (count <= 0) {
count = fTokenIdCounter = 1;
}
return count;
}
/**
* Write the str containing an MI message to the tracing stream.
* The str will generally have a newline already if toGdb is true,
* and will need a newline if toGdb is false.
* @param toGdb true if MI is to GDB, false if from GDB
* @param str string containing MI message
* @since 5.6
*/
protected void writeToTracingStream(boolean toGdb, final String str) {
if (getMITracingStream() != null) {
try {
String message = GdbPlugin.getDebugTime() + " " + str + (toGdb ? "" : "\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
IEclipsePreferences node = InstanceScope.INSTANCE.getNode(GdbPlugin.PLUGIN_ID);
boolean limitEnabled = node.getBoolean(IGdbDebugPreferenceConstants.PREF_MAX_MI_OUTPUT_LINES_ENABLE,
IGdbDebugPreferenceConstants.MAX_MI_OUTPUT_LINES_ENABLE_DEFAULT);
int initialMaxLines = IGdbDebugPreferenceConstants.MAX_MI_OUTPUT_LINES_DEFAULT;
try {
initialMaxLines = node.getInt(IGdbDebugPreferenceConstants.PREF_MAX_MI_OUTPUT_LINES,
IGdbDebugPreferenceConstants.MAX_MI_OUTPUT_LINES_DEFAULT);
} catch (NumberFormatException e) {
}
int linecounter = initialMaxLines;
while (message.length() > 100 && (!limitEnabled || linecounter-- > 0)) {
String partial = message.substring(0, 100);
message = message.substring(100);
getMITracingStream().write(partial.getBytes());
getMITracingStream().write("\\\n".getBytes()); //$NON-NLS-1$
}
if (linecounter <= 0) {
String messageTruncatedInfo = NLS.bind(Messages.AbstractMIControl_message_truncated,
initialMaxLines);
getMITracingStream().write(messageTruncatedInfo.getBytes());
} else {
getMITracingStream().write(message.getBytes());
}
} catch (IOException e) {
// The tracing stream could be closed at any time
// since the user can set a preference to turn off
// this tracing.
setMITracingStream(null);
}
}
}
/*
* Support class which creates a convenient wrapper for holding all information about an
* individual request.
*/
private class CommandHandle implements ICommandToken {
private MICommand<MIInfo> fCommand;
private DataRequestMonitor<MIInfo> fRequestMonitor;
private int fTokenId;
CommandHandle(MICommand<MIInfo> c, DataRequestMonitor<MIInfo> d) {
fCommand = c;
fRequestMonitor = d;
fTokenId = -1; // Only initialize to a real value when needed
}
@Override
public MICommand<MIInfo> getCommand() {
return fCommand;
}
public DataRequestMonitor<MIInfo> getRequestMonitor() {
return fRequestMonitor;
}
// This method allows us to generate the token Id when we area actually going to use
// it. It is meant to help order the token ids based on when commands will actually
// be sent
public void generateTokenId() {
fTokenId = getNewTokenId();
}
public Integer getTokenId() {
return fTokenId;
}
public int getStackFrameId() {
IFrameDMContext frameCtx = DMContexts.getAncestorOfType(fCommand.getContext(), IFrameDMContext.class);
if (frameCtx != null)
return frameCtx.getLevel();
return -1;
}
public String getThreadId() {
IMIExecutionDMContext execCtx = DMContexts.getAncestorOfType(fCommand.getContext(),
IMIExecutionDMContext.class);
if (execCtx != null)
return execCtx.getThreadId();
return null;
}
public String getGroupId() {
IMIContainerDMContext containerCtx = DMContexts.getAncestorOfType(fCommand.getContext(),
IMIContainerDMContext.class);
if (containerCtx != null)
return containerCtx.getGroupId();
return null;
}
@Override
public String toString() {
return Integer.toString(fTokenId) + fCommand;
}
}
/*
* This is the transmitter thread. When a command is given to this thread it has been
* considered to be sent, even if it has not actually been sent yet. This assumption
* makes it easier from state management. Whomever fill this pipeline handles all of
* the required state notification ( callbacks ). This thread simply physically gives
* the message to the backend.
*/
private class TxThread extends Thread {
final private OutputStream fOutputStream;
public TxThread(OutputStream outStream) {
super("MI TX Thread"); //$NON-NLS-1$
fOutputStream = outStream;
}
@Override
public void run() {
while (true) {
CommandHandle commandHandle = null;
try {
commandHandle = fTxCommands.take();
} catch (InterruptedException e) {
break; // Shutting down.
}
if (commandHandle == fTerminatorHandle) {
// There is a small possibility that a new command was inserted
// in the fRxCommands map after we cleared that map.
// Just to be safe, clear it again.
// We do this to avoid synchronizing the handling of fRxCommands
// because this is more efficient, as it happens only once at shutdown.
cancelRxCommands();
break; // Null command is an indicator that we're shutting down.
}
/*
* We note that this is an outstanding request at this point.
*/
if (!(commandHandle.getCommand() instanceof RawCommand)) {
// RawCommands will not get an answer, so we cannot put them in the receive queue.
fRxCommands.put(commandHandle.getTokenId(), commandHandle);
}
/*
* Construct the new command and push this command out the pipeline.
*/
final String str;
if (commandHandle.getCommand() instanceof RawCommand) {
// RawCommands CANNOT have a token id: GDB would read it as part of the RawCommand!
str = commandHandle.getCommand().constructCommand();
} else if (fUseThreadGroupOption) {
// Implies that fUseThreadAndFrameOptions == true
str = commandHandle.getTokenId() + commandHandle.getCommand().constructCommand(
commandHandle.getGroupId(), commandHandle.getThreadId(), commandHandle.getStackFrameId());
} else if (fUseThreadAndFrameOptions) {
str = commandHandle.getTokenId() + commandHandle.getCommand()
.constructCommand(commandHandle.getThreadId(), commandHandle.getStackFrameId());
} else {
str = commandHandle.getTokenId() + commandHandle.getCommand().constructCommand();
}
try {
if (fOutputStream != null) {
if (GdbDebugOptions.DEBUG) {
GdbDebugOptions.trace(
String.format("%s %s %s", GdbPlugin.getDebugTime(), MI_TRACE_IDENTIFIER, str)); //$NON-NLS-1$
}
writeToTracingStream(true, str);
fOutputStream.write(str.getBytes());
fOutputStream.flush();
}
} catch (IOException e) {
// Shutdown thread in case of IO error.
break;
}
}
// Must close the stream here to avoid leaking
// Bug 345164 and Bug 339379
try {
if (fOutputStream != null)
fOutputStream.close();
} catch (IOException e) {
}
}
}
private class RxThread extends Thread {
private final InputStream fInputStream;
private final MIParser fMiParser = new MIParser();
/**
* List of out of band records since the last result record. Out of band
* records are required for processing the results of CLI commands.
*/
private final List<MIOOBRecord> fAccumulatedOOBRecords = new LinkedList<>();
/**
* List of stream records since the last result record, not including
* the record currently being processed (if it's a stream one). This is
* a subset of {@link #fAccumulatedOOBRecords}, as a stream record is a
* particular type of OOB record.
*/
private final List<MIStreamRecord> fAccumulatedStreamRecords = new LinkedList<>();
public RxThread(InputStream inputStream) {
super("MI RX Thread"); //$NON-NLS-1$
fInputStream = inputStream;
}
@Override
public void run() {
BufferedReader reader = new BufferedReader(new InputStreamReader(fInputStream));
try {
String line;
while ((line = reader.readLine()) != null) {
if (line.length() != 0) {
// Write Gdb response to sysout or file
if (GdbDebugOptions.DEBUG) {
if (line.length() < DEVELOPMENT_TRACE_LIMIT_CHARS) {
GdbDebugOptions.trace(String.format("%s %s %s\n", GdbPlugin.getDebugTime(), //$NON-NLS-1$
MI_TRACE_IDENTIFIER, line));
} else {
// "-list-thread-groups --available" give a very large output that is not very useful but that makes
// looking at the traces much more difficult. Don't show the full output in the traces.
// If we really need to see that output, it will still be in the 'gdb traces'.
GdbDebugOptions.trace(String.format("%s %s %s\n", GdbPlugin.getDebugTime(), //$NON-NLS-1$
MI_TRACE_IDENTIFIER, line.substring(0, DEVELOPMENT_TRACE_LIMIT_CHARS)
+ " [remaining output truncated. Refer to 'gdb traces' if full output needed.]")); //$NON-NLS-1$
}
}
writeToTracingStream(false, line);
processMIOutput(line);
}
}
} catch (IOException e) {
// Socket is shut down.
} catch (RejectedExecutionException e) {
// Dispatch thread is down.
}
// Must close the stream here to avoid leaking and
// to give enough time to read all the data
// Bug 345164 and Bug 339379
try {
fInputStream.close();
} catch (IOException e) {
}
}
private MIResult findResultRecord(MIResult[] results, String variable) {
for (int i = 0; i < results.length; i++) {
if (variable.equals(results[i].getVariable())) {
return results[i];
}
}
return null;
}
private String getStatusString(MICommand<MIInfo> origCommand, MIOutput response) {
// Attempt to extract a message from the result record:
String message = null;
String[] parameters = null;
if (response != null && response.getMIResultRecord() != null) {
MIResult[] results = response.getMIResultRecord().getMIResults();
// Extract the parameters
MIResult paramsRes = findResultRecord(results, "parameters"); //$NON-NLS-1$
if (paramsRes != null && paramsRes.getMIValue() instanceof MIList) {
MIValue[] paramValues = ((MIList) paramsRes.getMIValue()).getMIValues();
parameters = new String[paramValues.length];
for (int i = 0; i < paramValues.length; i++) {
if (paramValues[i] instanceof MIConst) {
parameters[i] = ((MIConst) paramValues[i]).getString();
} else {
parameters[i] = ""; //$NON-NLS-1$
}
}
}
MIResult messageRes = findResultRecord(results, "message"); //$NON-NLS-1$
if (messageRes != null && messageRes.getMIValue() instanceof MIConst) {
message = ((MIConst) messageRes.getMIValue()).getString();
}
// FRCH: I believe that the actual string is "msg" ...
// FRCH: (at least for the version of gdb I'm using)
else {
messageRes = findResultRecord(results, "msg"); //$NON-NLS-1$
if (messageRes != null && messageRes.getMIValue() instanceof MIConst) {
message = ((MIConst) messageRes.getMIValue()).getString();
}
}
}
StringBuilder clientMsg = new StringBuilder();
clientMsg.append("Failed to execute MI command:\n"); //$NON-NLS-1$
clientMsg.append(origCommand.toString());
if (message != null) {
message = message.replaceAll("\\\\n", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
clientMsg.append("Error message from debugger back end:\n"); //$NON-NLS-1$
if (parameters != null) {
try {
clientMsg.append(MessageFormat.format(message, (Object[]) parameters));
} catch (IllegalArgumentException e2) {
// Message format string invalid. Fallback to just appending the strings.
clientMsg.append(message);
clientMsg.append(Arrays.toString(parameters));
}
} else {
clientMsg.append(message);
}
}
return clientMsg.toString();
}
private String getBackendMessage(MIOutput response) {
// Attempt to extract a message from the result record:
String message = null;
String[] parameters = null;
if (response != null && response.getMIResultRecord() != null) {
MIResult[] results = response.getMIResultRecord().getMIResults();
// Extract the parameters
MIResult paramsRes = findResultRecord(results, "parameters"); //$NON-NLS-1$
if (paramsRes != null && paramsRes.getMIValue() instanceof MIList) {
MIValue[] paramValues = ((MIList) paramsRes.getMIValue()).getMIValues();
parameters = new String[paramValues.length];
for (int i = 0; i < paramValues.length; i++) {
if (paramValues[i] instanceof MIConst) {
parameters[i] = ((MIConst) paramValues[i]).getString();
} else {
parameters[i] = ""; //$NON-NLS-1$
}
}
}
MIResult messageRes = findResultRecord(results, "message"); //$NON-NLS-1$
if (messageRes != null && messageRes.getMIValue() instanceof MIConst) {
message = ((MIConst) messageRes.getMIValue()).getString();
}
// FRCH: I believe that the actual string is "msg" ...
// FRCH: (at least for the version of gdb I'm using)
else {
messageRes = findResultRecord(results, "msg"); //$NON-NLS-1$
if (messageRes != null && messageRes.getMIValue() instanceof MIConst) {
message = ((MIConst) messageRes.getMIValue()).getString();
}
}
}
StringBuilder clientMsg = new StringBuilder();
if (message != null) {
if (parameters != null) {
try {
clientMsg.append(MessageFormat.format(message, (Object[]) parameters));
} catch (IllegalArgumentException e2) {
// Message format string invalid. Fallback to just appending the strings.
clientMsg.append(message);
clientMsg.append(Arrays.toString(parameters));
}
} else {
clientMsg.append(message);
}
}
return clientMsg.toString();
}
void processMIOutput(String line) {
MIParser.RecordType recordType = fMiParser.getRecordType(line);
if (recordType == MIParser.RecordType.ResultRecord) {
final MIResultRecord rr = fMiParser.parseMIResultRecord(line);
/*
* Find the command in the current output list. If we cannot then this is
* some form of asynchronous notification. Or perhaps general IO.
*/
int id = rr.getToken();
final CommandHandle commandHandle = fRxCommands.remove(id);
if (commandHandle != null) {
final MIOutput response = new MIOutput(rr,
fAccumulatedOOBRecords.toArray(new MIOOBRecord[fAccumulatedOOBRecords.size()]));
fAccumulatedOOBRecords.clear();
fAccumulatedStreamRecords.clear();
MIInfo result = commandHandle.getCommand().getResult(response);
DataRequestMonitor<MIInfo> rm = commandHandle.getRequestMonitor();
/*
* Not all users want to get there results. They indicate so by not having
* a completion object.
*/
if (rm != null) {
rm.setData(result);
/*
* We need to indicate if this request had an error or not.
*/
String errorResult = rr.getResultClass();
if (errorResult.equals(MIResultRecord.ERROR)) {
String status = getStatusString(commandHandle.getCommand(), response);
String message = getBackendMessage(response);
Exception exception = new Exception(message);
rm.setStatus(
new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, REQUEST_FAILED, status, exception));
}
/*
* We need to complete the command on the DSF thread for data security.
*/
final ICommandResult finalResult = result;
getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
/*
* Complete the specific command.
*/
if (commandHandle.getRequestMonitor() != null) {
commandHandle.getRequestMonitor().done();
}
/*
* Now tell the generic listeners about it.
*/
processCommandDone(commandHandle, finalResult);
}
@Override
public String toString() {
return "MI command output received for: " + commandHandle.getCommand(); //$NON-NLS-1$
}
});
} else {
/*
* While the specific requestor did not care about the completion we
* need to call any listeners. This could have been a CLI command for
* example and the CommandDone listeners there handle the IO as part
* of the work.
*/
final ICommandResult finalResult = result;
getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
processCommandDone(commandHandle, finalResult);
}
@Override
public String toString() {
return "MI command output received for: " + commandHandle.getCommand(); //$NON-NLS-1$
}
});
}
} else {
/*
* GDB apparently can sometimes send multiple responses to the same command. In those cases,
* the command handle is gone, so post the result as an event. To avoid processing OOB records
* as events multiple times, do not include the accumulated OOB record list in the response
* MIOutput object.
*/
final MIOutput response = new MIOutput(rr, new MIOOBRecord[0]);
getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
processEvent(response);
}
@Override
public String toString() {
return "MI asynchronous output received: " + response; //$NON-NLS-1$
}
});
}
} else if (recordType == MIParser.RecordType.OOBRecord) {
// Process OOBs
final MIOOBRecord oob = fMiParser.parseMIOOBRecord(line);
fAccumulatedOOBRecords.add(oob);
// limit growth, but only if these are not responses to CLI commands
// Bug 302927 & 330608
if (fRxCommands.isEmpty() && fAccumulatedOOBRecords.size() > 20) {
fAccumulatedOOBRecords.remove(0);
}
// The handling of this OOB record may need the stream records
// that preceded it. One such case is a stopped event caused by a
// catchpoint in gdb < 7.0. The stopped event provides no
// reason, but we can determine it was caused by a catchpoint by
// looking at the target stream.
final MIOutput response = new MIOutput(oob,
fAccumulatedStreamRecords.toArray(new MIStreamRecord[fAccumulatedStreamRecords.size()]));
// If this is a stream record, add it to the accumulated bucket
// for possible use in handling a future OOB (see comment above)
if (oob instanceof MIStreamRecord) {
fAccumulatedStreamRecords.add((MIStreamRecord) oob);
if (fAccumulatedStreamRecords.size() > 20) { // limit growth; see bug 302927
fAccumulatedStreamRecords.remove(0);
}
}
/*
* OOBS are events. So we pass them to any event listeners who want to see them. Again this must
* be done on the DSF thread for integrity.
*/
getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
processEvent(response);
}
@Override
public String toString() {
return "MI asynchronous output received: " + response; //$NON-NLS-1$
}
});
}
getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
processNextQueuedCommand();
}
});
}
}
/**
* A thread that will read GDB's stderr stream.
* When a PTY is not being used for the inferior, everything
* the inferior writes to stderr will be output on GDB's stderr.
* If we don't read it, gdb eventually blocks, when the sream is
* full.
*
* Although we could write this error output to the inferior
* console, we actually write it to the GDB console. This is
* because we cannot differentiate between inferior errors printouts
* and GDB error printouts.
*
* See bug 327617 for details.
*/
private class ErrorThread extends Thread {
private final InputStream fErrorStream;
private final MIParser fMiParser = new MIParser();
public ErrorThread(InputStream errorStream) {
super("MI Error Thread"); //$NON-NLS-1$
fErrorStream = errorStream;
}
@Override
public void run() {
BufferedReader reader = new BufferedReader(new InputStreamReader(fErrorStream));
try {
String line;
while ((line = reader.readLine()) != null) {
// Create an error MI out-of-band record so that our gdb console prints it.
final MIOOBRecord oob = fMiParser.parseMIOOBRecord("&" + line + "\n"); //$NON-NLS-1$//$NON-NLS-2$
final MIOutput response = new MIOutput(oob, new MIStreamRecord[0]);
getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
processEvent(response);
}
@Override
public String toString() {
return "MI error output received: " + response; //$NON-NLS-1$
}
});
}
} catch (IOException e) {
// Socket is shut down.
} catch (RejectedExecutionException e) {
// Dispatch thread is down.
}
// Must close the stream here to avoid leaking
// Bug 345164 and Bug 339379
try {
fErrorStream.close();
} catch (IOException e) {
}
}
}
// we keep track of currentStackLevel and currentThreadId because in
// some cases we must use -thread-select and -stack-select-frame
public void resetCurrentThreadLevel() {
fCurrentThreadId = null;
}
public void resetCurrentStackLevel() {
fCurrentStackLevel = -1;
}
/**
* @since 4.1
*/
@ConfinedToDsfExecutor("this.getExecutor()")
protected void commandFailed(ICommandToken token, int statusCode, String errorMessage) {
if (!(token instanceof CommandHandle && token.getCommand() instanceof MICommand<?>))
return;
final CommandHandle commandHandle = (CommandHandle) token;
Integer tokenId = commandHandle.getTokenId();
// If the timeout value is too small a command can be timed out but still processed by RxThread.
// To avoid processing it twice we need to remove it from the command list.
CommandHandle h = fRxCommands.remove(tokenId);
if (h == null)
// Command has already been processed by RxThread.
return;
MIConst value = new MIConst();
value.setCString(errorMessage);
MIResult result = new MIResult();
result.setVariable("msg"); //$NON-NLS-1$
result.setMIValue(value);
MIResultRecord resultRecord = new MIResultRecord();
resultRecord.setToken(tokenId.intValue());
resultRecord.setResultClass(MIResultRecord.ERROR);
resultRecord.setMIResults(new MIResult[] { result });
MIOutput miOutput = new MIOutput(resultRecord, new MIOOBRecord[0]);
final MIInfo info = commandHandle.getCommand().getResult(miOutput);
DataRequestMonitor<MIInfo> rm = commandHandle.getRequestMonitor();
if (rm != null) {
rm.setData(info);
rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, statusCode, errorMessage, null));
rm.done();
/*
* Now tell the generic listeners about it.
*/
processCommandDone(commandHandle, info);
}
}
/**
* @since 6.1
*/
@Override
public void flushAllCachesAndRefresh(RequestMonitor rm) {
DsfServicesTracker servicesTracker = getServicesTracker();
Set<ICachingService> services = new HashSet<>(servicesTracker.getServices(ICachingService.class));
for (ICachingService service : services) {
service.flushCache(null);
}
// Issue a refresh event for any services or UI that is not an ICachingService
getSession().dispatchEvent(new RefreshAllDMEvent(getContext()), getProperties());
rm.done();
}
}