Package uk.co.cwspencer.gdb

Source Code of uk.co.cwspencer.gdb.Gdb$GdbEventCallback

package uk.co.cwspencer.gdb;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import ro.redeul.google.go.config.sdk.GoSdkData;
import ro.redeul.google.go.sdk.GoSdkUtil;
import uk.co.cwspencer.gdb.gdbmi.*;
import uk.co.cwspencer.gdb.messages.*;
import uk.co.cwspencer.ideagdb.debug.GdbDebugProcess;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.*;

/**
* Class for interacting with GDB.
*/
public class Gdb {
    private static final Logger m_log = Logger.getInstance("#uk.co.cwspencer.gdb.Gdb");

    // Size in KB for the buffer
    private static final int BUFFER_SIZE = 256 * 1024;

    /**
     * Interface for callbacks for results from completed GDB commands.
     */
    public interface GdbEventCallback {
        /**
         * Called when a response to the command is received.
         *
         * @param event The event.
         */
        public void onGdbCommandCompleted(GdbEvent event);
    }

    // Information about a command that is awaiting processing
    private class CommandData {
        // The command
        String command;
        // The user provided callback; may be null
        GdbEventCallback callback;

        CommandData(String command, GdbEventCallback callback) {
            this.command = command;
            this.callback = callback;
        }
    }

    // Handle to the ASCII character set
    private static Charset m_ascii = Charset.forName("US-ASCII");

    // The listener
    private GdbListener m_listener;

    // Handle for the GDB process
    private Process m_process;

    // Threads which read/write data from GDB
    private Thread m_readThread;
    private Thread m_writeThread;

    // Flag indicating whether we are stopping
    private Boolean m_stopping = false;

    // Flag indicating whether we have received the first record from GDB yet
    private boolean m_firstRecord = true;

    // Token which the next GDB command will be sent with
    private long m_token = 1;

    // Commands that are waiting to be sent
    private List<CommandData> m_queuedCommands = new ArrayList<CommandData>();

    // Commands that have been sent to GDB and are awaiting a response
    private final Map<Long, CommandData> m_pendingCommands = new HashMap<Long, CommandData>();

    // GDB variable objects
    private final Map<String, GdbVariableObject> m_variableObjectsByExpression =
            new HashMap<String, GdbVariableObject>();
    private final Map<String, GdbVariableObject> m_variableObjectsByName =
            new HashMap<String, GdbVariableObject>();

    // List of capabilities supported by GDB
    private Set<String> m_capabilities;

    /**
     * Constructor; prepares GDB.
     *
     * @param gdbPath          The path to the GDB executable.
     * @param workingDirectory Working directory to launch the GDB process in. May be null.
     * @param listener         Listener that is to receive GDB events.
     */
    public Gdb(final String gdbPath, final String workingDirectory, GdbListener listener) {
        // Prepare GDB
        m_listener = listener;
        m_readThread = new Thread(new Runnable() {
            @Override
            public void run() {
                runGdb(gdbPath, workingDirectory);
            }
        });
    }

    /**
     * Finalizer. Kills GDB and the I/O threads.
     */
    @Override
    protected synchronized void finalize() throws Throwable {
        super.finalize();

        // Terminate the I/O threads
        if (m_readThread != null) {
            m_readThread.interrupt();
            m_readThread.join();
        }
        if (m_writeThread != null) {
            m_stopping = true;
            m_writeThread.notify();
            m_writeThread.interrupt();
            m_writeThread.join();
        }

        // Kill the process
        if (m_process != null) {
            m_process.destroy();
        }
    }

    /**
     * Starts GDB.
     */
    public void start() {
        if (m_readThread.isAlive()) {
            throw new IllegalStateException("GDB has already been started");
        }
        m_readThread.start();
    }

    /**
     * Sends an arbitrary command to GDB.
     *
     * @param command The command to send. This may be a normal CLI command or a GDB/MI command. It
     *                should not contain any line breaks.
     */
    public void sendCommand(String command) {
        sendCommand(command, null);
    }

    /**
     * Sends an arbitrary command to GDB and requests a completion callback.
     *
     * @param command  The command to send. This may be a normal CLI command or a GDB/MI command. It
     *                 should not contain any line breaks.
     * @param callback The callback function.
     */
    public synchronized void sendCommand(String command, GdbEventCallback callback) {
        // Queue the command
        if (command.equals("-gdb-exit")) {
            m_queuedCommands.clear();
        }

        m_queuedCommands.add(new CommandData(command, callback));
        notify();
    }

    /**
     * Indicates whether GDB has the given capability.
     *
     * @param capability The capability to check for.
     * @return Whether GDB has the capability.
     */
    public synchronized boolean hasCapability(String capability) {

        final String cap = capability;

        if (m_capabilities == null) {
            new Runnable() {
                public void run() {
                    m_log.warn("Capabilities list is null; returning 'unsupported' for capability " + cap);
                }
            };

            return false;
        }

        return m_capabilities.contains(capability);
    }

    /**
     * Gets information about the local variables for the given stack frame.
     *
     * @param thread   The thread on which the frame resides.
     * @param frame    The frame number.
     * @param callback The callback function. This is passed a GdbVariableObjects value on success,
     *                 or GdbErrorEvent on failure.
     */
    public void getVariablesForFrame(final int thread, final int frame,
                                     final GdbEventCallback callback) {
        // Get a list of local variables
        String command = "-stack-list-variables --thread " + thread + " --frame " + frame +
                " --no-values";
        sendCommand(command, new GdbEventCallback() {
            @Override
            public void onGdbCommandCompleted(GdbEvent event) {
                onGdbVariablesReady(event, thread, frame, callback);
            }
        });
    }

    /**
     * Evaluates the given expression in the given context.
     *
     * @param thread     The thread to evaluate the expression in.
     * @param frame      The frame to evaluate the expression in.
     * @param expression The expression to evaluate.
     * @param callback   The callback function.
     */
    public void evaluateExpression(int thread, int frame, final String expression,
                                   final GdbEventCallback callback) {
        // TODO: Make this more efficient

        // Create a new variable object if necessary
        GdbVariableObject variableObject = m_variableObjectsByExpression.get(expression);
        if (variableObject == null) {
            String command = "-var-create --thread " + thread + " --frame " + frame + " - @ " +
                    formatVarName(expression);
            sendCommand(command, new GdbEventCallback() {
                @Override
                public void onGdbCommandCompleted(GdbEvent event) {
                    onGdbNewVariableObjectReady(event, expression, callback);
                }
            });
        }

        // Update existing variable objects
        sendCommand("-var-update --thread " + thread + " --frame " + frame + " --all-values *",
                new GdbEventCallback() {
                    @Override
                    public void onGdbCommandCompleted(GdbEvent event) {
                        HashSet<String> expressions = new HashSet<String>();
                        expressions.add(expression);
                        onGdbVariableObjectsUpdated(event, expressions, callback);
                    }
                });
    }

    /**
     * Launches the GDB process and starts listening for data.
     *
     * @param gdbPath          Path to the GDB executable.
     * @param workingDirectory Working directory to launch the GDB process in. May be null.
     */
    private void runGdb(String gdbPath, String workingDirectory) {
        try {
            // Launch the process
            final String[] commandLine = {
                    gdbPath,
                    "--interpreter=mi2",
            };

            File workingDirectoryFile = null;
            if (workingDirectory != null) {
                workingDirectoryFile = new File(workingDirectory);
            }

            Project project = ((GdbDebugProcess) m_listener).m_project;

            Sdk sdk = GoSdkUtil.getGoogleGoSdkForProject(project);
            if ( sdk == null ) {
                return;
            }

            final GoSdkData sdkData = (GoSdkData)sdk.getSdkAdditionalData();
            if ( sdkData == null ) {
                return;
            }

            String projectDir = project.getBasePath();

            if (projectDir == null) {
                return;
            }

            String[] goEnv = GoSdkUtil.getExtendedGoEnv(sdkData, projectDir, "");

            Process process = Runtime.getRuntime().exec(commandLine, goEnv, workingDirectoryFile);
            InputStream stream = process.getInputStream();

            // Save a reference to the process and launch the writer thread
            synchronized (this) {
                m_process = process;
                m_writeThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        processWriteQueue();
                    }
                });
                m_writeThread.start();
            }

            // Start listening for data
            //GdbMiParser parser = new GdbMiParser();
            GdbMiParser2 parser = new GdbMiParser2(((GdbDebugProcess) m_listener).m_gdbRawConsole);
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytes;
            while ((bytes = stream.read(buffer)) != -1) {
                // Process the data
                try {
                    parser.process(buffer, bytes);
                    buffer = new byte[BUFFER_SIZE];
                } catch (IllegalArgumentException ex) {
                    m_log.error("GDB/MI parsing error. Current buffer contents: \"" +
                            new String(buffer, 0, bytes) + "\"", ex);
                    m_listener.onGdbError(ex);
                    return;
                }

                // Handle the records
                List<GdbMiRecord> records = parser.getRecords();
                for (GdbMiRecord record : records) {
                    handleRecord(record);
                }
                records.clear();
            }
        } catch (Throwable ex) {
            m_listener.onGdbError(ex);
        }
    }

    /**
     * Thread function for processing the write queue.
     */
    private void processWriteQueue() {
        try {
            OutputStream stream;
            List<CommandData> queuedCommands = new ArrayList<CommandData>();
            while (true) {
                synchronized (this) {
                    // Wait for some data to process
                    while (m_queuedCommands.isEmpty()) {
                        wait();
                    }

                    // Exit cleanly if we are stopping
                    if (m_stopping) {
                        return;
                    }

                    // Do the processing we need before dropping out of synchronised mode
                    stream = m_process.getOutputStream();

                    // Insert the commands into the pending queue
                    long token = m_token;
                    for (CommandData command : m_queuedCommands) {
                        m_pendingCommands.put(token++, command);
                    }

                    // Swap the queues
                    List<CommandData> tmp = queuedCommands;
                    queuedCommands = m_queuedCommands;
                    m_queuedCommands = tmp;
                }

                // Send the queued commands to GDB
                StringBuilder sb = new StringBuilder();
                for (CommandData command : queuedCommands) {
                    // Construct the message
                    long token = m_token++;
                    m_listener.onGdbCommandSent(command.command, token);

                    sb.append(token);
                    sb.append(command.command);
                    sb.append("\r\n");
                }
                queuedCommands.clear();

                // Send the messages
                byte[] message = sb.toString().getBytes(m_ascii);
                stream.write(message);
                stream.flush();
            }
        } catch (InterruptedException ex) {
            // We are exiting
        } catch (Throwable ex) {
            m_listener.onGdbError(ex);
        }
    }

    /**
     * Handles the given GDB/MI record.
     *
     * @param record The record.
     */
    private void handleRecord(GdbMiRecord record) {
        switch (record.type) {
            case Target:
            case Console:
            case Log:
                handleStreamRecord((GdbMiStreamRecord) record);
                break;

            case Immediate:
            case Exec:
            case Notify:
            case Status:
                handleResultRecord((GdbMiResultRecord) record);
                break;
        }

        // If this is the first record we have received we know we are fully started, so notify the
        // listener
        if (m_firstRecord) {
            m_firstRecord = false;
            m_listener.onGdbStarted();
        }
    }

    /**
     * Handles the given GDB/MI stream record.
     *
     * @param record The record.
     */
    private void handleStreamRecord(GdbMiStreamRecord record) {
        // Notify the listener
        m_listener.onStreamRecordReceived(record);
    }

    /**
     * Handles the given GDB/MI result record.
     *
     * @param record The record.
     */
    private void handleResultRecord(GdbMiResultRecord record) {
        // Notify the listener
        m_listener.onResultRecordReceived(record);

        // Find the pending command data
        CommandData pendingCommand = null;
        String commandType = null;
        if (record.userToken != null) {
            synchronized (this) {
                pendingCommand = m_pendingCommands.remove(record.userToken);
            }
            if (pendingCommand != null) {
                // Get the command type
                int separatorIndex = pendingCommand.command.indexOf(' ');
                commandType = separatorIndex == -1 ? pendingCommand.command :
                        pendingCommand.command.substring(0, separatorIndex);
            }
        }

        // Process the event into something more useful
        GdbEvent event = GdbMiMessageConverter.processRecord(record, commandType);
        if (event != null) {
            // Notify the listener
            m_listener.onGdbEventReceived(event);
            if (pendingCommand != null && pendingCommand.callback != null) {
                pendingCommand.callback.onGdbCommandCompleted(event);
            }
        }
    }

    /**
     * Callback function for when GDB has responded to our stack variables request.
     *
     * @param event    The event.
     * @param thread   The thread on which the frame resides.
     * @param frame    The frame number.
     * @param callback The user-provided callback function.
     */
    private void onGdbVariablesReady(GdbEvent event, int thread, int frame,
                                     final GdbEventCallback callback) {
        if (event instanceof GdbErrorEvent) {
            callback.onGdbCommandCompleted(event);
            return;
        }
        if (!(event instanceof GdbVariables)) {
            GdbErrorEvent errorEvent = new GdbErrorEvent();
            errorEvent.message = "Unexpected data received from GDB";
            callback.onGdbCommandCompleted(errorEvent);
            m_log.warn("Unexpected event " + event + " received from -stack-list-variables " +
                    "request");
            return;
        }

        // Create variable objects for each of the variables if we haven't done so already
        final GdbVariables variables = (GdbVariables) event;
        for (final String variable : variables.variables.keySet()) {
            GdbVariableObject variableObject = m_variableObjectsByExpression.get(variable);
            if (variableObject == null) {
                String command = "-var-create --thread " + thread + " --frame " + frame + " - @ " +
                        formatVarName(variable);
                sendCommand(command, new GdbEventCallback() {
                    @Override
                    public void onGdbCommandCompleted(GdbEvent event) {
                        onGdbNewVariableObjectReady(event, variable, callback);
                    }
                });
            }
        }

        // Update any existing variable objects
        sendCommand("-var-update --thread " + thread + " --frame " + frame + " --all-values *",
                new GdbEventCallback() {
                    @Override
                    public void onGdbCommandCompleted(GdbEvent event) {
                        onGdbVariableObjectsUpdated(event, variables.variables.keySet(), callback);
                    }
                });
    }

    /**
     * Callback function for when GDB has responded to our new variable object request.
     *
     * @param event      The event.
     * @param expression The expression used to create the variable object.
     * @param callback   The user-provided callback function.
     */
    private void onGdbNewVariableObjectReady(GdbEvent event, String expression,
                                             GdbEventCallback callback) {
        if (event instanceof GdbErrorEvent) {
            callback.onGdbCommandCompleted(event);
            return;
        }
        if (!(event instanceof GdbVariableObject)) {
            GdbErrorEvent errorEvent = new GdbErrorEvent();
            errorEvent.message = "Unexpected data received from GDB";
            callback.onGdbCommandCompleted(errorEvent);
            m_log.warn("Unexpected event " + event + " received from -var-create request");
            return;
        }

        GdbVariableObject variableObject = (GdbVariableObject) event;
        if (variableObject.name == null) {
            GdbErrorEvent errorEvent = new GdbErrorEvent();
            errorEvent.message = "Unexpected data received from GDB";
            callback.onGdbCommandCompleted(errorEvent);
            m_log.warn("Variable object returned by GDB does not have a name");
            return;
        }

        // Save the new variable object
        variableObject.expression = expression;
        m_variableObjectsByExpression.put(expression, variableObject);
        m_variableObjectsByName.put(variableObject.name, variableObject);
    }

    /**
     * Callback function for when GDB has responded to our variable objects update request.
     *
     * @param event     The event.
     * @param variables The variables the user requested.
     * @param callback  The user-provided callback function.
     */
    private void onGdbVariableObjectsUpdated(GdbEvent event, Set<String> variables,
                                             GdbEventCallback callback) {
        if (event instanceof GdbErrorEvent) {
            callback.onGdbCommandCompleted(event);
            return;
        }
        if (!(event instanceof GdbVariableObjectChanges)) {
            GdbErrorEvent errorEvent = new GdbErrorEvent();
            errorEvent.message = "Unexpected data received from GDB";
            callback.onGdbCommandCompleted(errorEvent);
            m_log.warn("Unexpected event " + event + " received from -var-create request");
            return;
        }

        // Update variable objects with changes
        GdbVariableObjectChanges changes = (GdbVariableObjectChanges) event;
        if (changes.changes != null) {
            for (GdbVariableObjectChange change : changes.changes) {
                if (change.name == null) {
                    m_log.warn("Received a GDB variable object change with no name");
                    continue;
                }

                GdbVariableObject variableObject = m_variableObjectsByName.get(change.name);
                if (variableObject == null) {
                    m_log.warn("Received a GDB variable object change for a variable object " +
                            "that does not exist");
                    continue;
                }

                // Set the new value
                switch (change.inScope) {
                    case True:
                        // Update the value
                        variableObject.value = change.value;
                        break;

                    case False:
                        // Reset the value
                        variableObject.value = null;
                        break;

                    default:
                        // TODO: Delete the variable object
                        variableObject.value = null;
                }

                // Set the new type
                if (change.typeChanged && change.newType != null) {
                    variableObject.type = change.newType;
                }
            }
        }

        // Construct the list of variable object the user requested
        GdbVariableObjects list = new GdbVariableObjects();
        list.objects = new ArrayList<GdbVariableObject>();
        for (String expression : variables) {
            GdbVariableObject object = m_variableObjectsByExpression.get(expression);
            if (object != null) {
                list.objects.add(object);
            }
        }

        callback.onGdbCommandCompleted(list);
    }

    /**
     * Callback function for when GDB has responded to our list capabilities request.
     *
     * @param event The event.
     */
    public void onGdbCapabilitiesReady(GdbEvent event) {
        if (event instanceof GdbErrorEvent) {
            m_log.warn("Failed to get GDB capabilities list: " + ((GdbErrorEvent) event).message);
            return;
        }
        if (!(event instanceof GdbFeatures)) {
            m_log.warn("Unexpected event " + event + " received from -list-features request");
            return;
        }

        // Save the list
        GdbFeatures features = (GdbFeatures) event;
        if (features.features != null) {
            Set<String> capabilities = new HashSet<String>(features.features);
            synchronized (this) {
                m_capabilities = capabilities;
            }
        }
    }

    private static String formatVarName(String varName) {
        varName = GdbMiUtil.formatGdbString(varName, false);

        if (varName.substring(0, 1).equals("&")) {
            varName = String.format("`%s`", varName);
        }

        return varName;
    }
}
TOP

Related Classes of uk.co.cwspencer.gdb.Gdb$GdbEventCallback

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.