| /******************************************************************************* |
| * 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.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 synchronized void cancelRxCommands() { |
| for (CommandHandle commandHandle : fRxCommands.values()) { |
| if (commandHandle.getRequestMonitor() == null) |
| continue; |
| commandHandle.getRequestMonitor().setStatus(genStatus("Connection is shut down")); //$NON-NLS-1$ |
| commandHandle.getRequestMonitor().done(); |
| } |
| fRxCommands.clear(); |
| } |
| |
| /** |
| * 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(); |
| } |
| } |