SquirrelJME/SquirrelJME

View on GitHub
modules/debug-jdwp/src/main/java/cc/squirreljme/jdwp/JDWPCommLink.java

Summary

Maintainability
A
2 hrs
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------

package cc.squirreljme.jdwp;

import cc.squirreljme.runtime.cldc.debug.Debugging;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;

/**
 * This handles the input and output communication of JDWPa.
 *
 * @since 2021/03/09
 */
public final class JDWPCommLink
    implements Closeable
{
    /** Should debugging be enabled? */
    public static final boolean DEBUG =
        Boolean.getBoolean("cc.squirreljme.jdwp.debug");
    
    /** Handshake sequence, sent by both sides. */
    private static final byte[] _HANDSHAKE_SEQUENCE =
        {'J', 'D', 'W', 'P', '-', 'H', 'a', 'n', 'd', 's', 'h', 'a', 'k', 'e'};
    
    /** Initial data buffer length. */
    private static final int _INIT_DATA_LEN =
        1024;
    
    /** The size of the packet header. */
    static final int _HEADER_SIZE =
        11;
    
    /** The input communication stream. */
    protected final DataInputStream in;
    
    /** The output communication stream. */
    protected final DataOutputStream out;
    
    /** The direction of the channel. */
    protected final JDWPCommLinkDirection direction;
    
    /** The queue of packets which are freed, they will go back here. */
    private final Deque<JDWPPacket> _freePackets =
        new LinkedList<>();
    
    /** The header bytes. */
    private final byte[] _header =
        new byte[JDWPCommLink._HEADER_SIZE];
    
    /** The monitor used for the output object. */
    private final Object _outMonitor =
        new Object();
    
    /** Identifier sizes, needed for reading IDs. */
    private volatile JDWPIdSizes _idSizes;
    
    /** Read position for the header. */
    private volatile int _headerAt;
    
    /** The data. */
    private volatile byte[] _data =
        new byte[JDWPCommLink._INIT_DATA_LEN];
    
    /** Read position for read data. */
    private volatile int _dataAt;
    
    /** Length of read data. */
    private volatile int _dataLen =
        -1;
    
    /** Did we do our handshake? */
    private volatile boolean _didHandshake;
    
    /** Are we in shutdown? */
    volatile boolean _shutdown;
    
    /** Next packet ID number. */
    private volatile int _nextId;
    
    /**
     * Initializes the communication link.
     *
     * @param __in The input stream to read from.
     * @param __out The output stream to write to.
     * @throws NullPointerException On null arguments.
     * @since 2021/03/08
     */
    public JDWPCommLink(InputStream __in, OutputStream __out)
        throws NullPointerException
    {
        this(__in, __out, JDWPCommLinkDirection.CLIENT_TO_DEBUGGER);
    }
    
    /**
     * Initializes the communication link.
     *
     * @param __in The input stream to read from.
     * @param __out The output stream to write to.
     * @param __direction The direction of communication.
     * @throws NullPointerException On null arguments.
     * @since 2021/03/08
     */
    public JDWPCommLink(InputStream __in, OutputStream __out,
        JDWPCommLinkDirection __direction)
        throws NullPointerException
    {
        if (__in == null || __out == null || __direction == null)
            throw new NullPointerException("NARG");
        
        this.in = new DataInputStream(__in);
        this.out = new DataOutputStream(__out);
        this.direction = __direction;
    }
    
    /**
     * Are the ID sizes now known?
     *
     * @return If the sizes are known.
     * @since 2024/01/23
     */
    public boolean areSizesKnown()
    {
        synchronized (this)
        {
            return this._idSizes != null;
        }
    }
    
    /**
     * {@inheritDoc}
     * @since 2021/03/08
     */
    @Override
    public void close()
        throws JDWPException
    {
        IOException fail = null;
        
        // Enter shut-down mode
        synchronized (this)
        {
            this._shutdown = true;
        }
        
        // Close the input
        try
        {
            this.in.close();
        }
        catch (IOException e)
        {
            fail = e;
        }
        
        // And the output
        try
        {
            this.out.close();
        }
        catch (IOException e)
        {
            if (fail == null)
                fail = e;
            else
                fail.addSuppressed(e);
        }
        
        /* {@squirreljme.error AG09 Could not close communication link.} */
        if (fail != null)
            throw new JDWPException("AG09", fail);
    }
    
    /**
     * Gets a blank packet.
     *
     * @return A blank packet.
     * @since 2024/01/22
     */
    public JDWPPacket getPacket()
    {
        return this.__getPacket(true);
    }
    
    /**
     * Returns the ID sizes of the communication link.
     *
     * @return The link's ID sizes.
     * @since 2024/01/23
     */
    public JDWPIdSizes idSizes()
    {
        synchronized (this)
        {
            return this._idSizes;
        }
    }
    
    /**
     * Is the debug link shutdown?
     *
     * @return If this is shutdown.
     * @since 2024/01/19
     */
    public boolean isShutdown()
    {
        synchronized (this)
        {
            return this._shutdown;
        }
    }
    
    /**
     * The next ID number.
     * 
     * @return Returns a new ID number.
     * @since 2021/03/13
     */
    public final int nextId()
    {
        synchronized (this)
        {
            return ++this._nextId;
        }
    }
    
    /**
     * Polls for the next event, if there is any.
     * 
     * @throws JDWPException If there is an issue with the connection.
     * @return A packet or {@code null} if there are none.
     * @since 2021/03/10
     */
    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
    public JDWPPacket poll()
        throws JDWPException
    {
        // If the handshake did not happen, do it now
        synchronized (this)
        {
            if (!this._didHandshake)
                this.__handshake();
        }
        
        // Used to read in the header as needed
        byte[] header = this._header;
        int headerAt = this._headerAt;
        
        // Data and whatever length
        byte[] data = this._data;
        int dataLen = this._dataLen;
        int dataAt = this._dataAt;
        
        // Constant reading in loop
        for (InputStream in = this.in;;)
            try
            {
                // Shutting down?
                if (this._shutdown)
                    return null;
                
                // Still reading in the header?
                int headerLeft = JDWPCommLink._HEADER_SIZE - headerAt;
                if (headerLeft > 0)
                {
                    int rc = in.read(header, headerAt, headerLeft);
                    
                    // EOF?
                    if (rc < 0)
                    {
                        /* {@squirreljme.error AG07 Short header read.} */
                        if (headerAt > 0)
                            throw new EOFException("AG07");
                            
                        this._shutdown = true;
                        return null;
                    }
                    
                    headerLeft -= rc;
                    headerAt += rc;
                }
                
                // Still need the header to be read?
                if (headerLeft > 0)
                    continue;
                
                // Do not know the data length yet?
                if (dataLen < 0)
                {
                    // Figure out the data length, note that it includes our
                    // own header!!
                    dataLen = Math.max((((header[0] & 0xFF) << 24) |
                        ((header[1] & 0xFF) << 16) |
                        ((header[2] & 0xFF) << 8) |
                        (header[3] & 0xFF)) - 11, 0);
                        
                    // If our buffer is too small, grow it just enough to fit
                    if (dataLen > data.length)
                        data = new byte[dataLen];
                }
                
                // Read in any associated data
                int dataLeft = dataLen - dataAt;
                if (dataLen >= 0 && dataLeft > 0)
                {
                    int rc = in.read(data, dataAt, dataLeft);
                    
                    // EOF?
                    if (rc < 0)
                    {
                        /* {@squirreljme.error AG08 Short header read.} */
                        if (dataAt > 0)
                            throw new EOFException("AG08");
                            
                        this._shutdown = true;
                        return null;
                    }
                    
                    dataLeft -= rc;
                    dataAt += rc;
                }
                
                // Still need more data to be read
                if (dataLen >= 0 && dataLeft > 0)
                    continue;
                
                // Setup a fresh packet
                JDWPPacket packet = this.__getPacket(false);
                packet.__load(header, data, dataLen);
                
                // Reset state for the next run
                headerAt = 0;
                dataAt = 0;
                dataLen = -1;
                
                // This packet is ready so use it now
                return packet;
            }
            
            // If we get interrupted, we just either shutdown or continue
            catch (InterruptedIOException ignored)
            {
                return null;
            }
    
            /* {@squirreljme.error AG06 Read error.} */
            catch (IOException e)
            {
                // Shutdown the link
                this._shutdown = true;
                
                // Fail here
                throw new JDWPException("AG06", e);
            }
            
            // Store resultant state
            finally
            {
                this._headerAt = headerAt;
                this._data = data;
                this._dataLen = dataLen;
                this._dataAt = dataAt;
            }
    }
    
    /**
     * Creates a reply packet.
     *
     * @param __id The raw packet ID that is being responded to.
     * @param __error The error to use for the packet.
     * @return The resultant reply packet.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/19
     */
    public JDWPPacket reply(int __id, JDWPErrorType __error)
        throws NullPointerException
    {
        if (__error == null)
            throw new NullPointerException("NARG");
        
        JDWPPacket rv = this.__getPacket(true);
        
        rv._id = __id;
        rv._errorCode = __error;
        rv._rawErrorCode = __error.id;
        rv._flags = JDWPPacket.FLAG_REPLY;
        
        return rv;
    }
    
    /**
     * Creates a reply packet.
     *
     * @param __packet The packet to reply to.
     * @param __error The error to use for the packet.
     * @return The resultant reply packet.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/19
     */
    public JDWPPacket reply(JDWPPacket __packet, JDWPErrorType __error)
        throws NullPointerException
    {
        if (__packet == null || __error == null)
            throw new NullPointerException("NARG");
        
        return this.reply(__packet.id(), __error);
    }
    
    /**
     * Creates a packet for a request.
     *
     * @param __commandSet The command set to use.
     * @param __command The command to use.
     * @return The newly created packet.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/19
     */
    public JDWPPacket request(JDWPCommandSet __commandSet,
        JDWPCommand __command)
        throws NullPointerException
    {
        if (__commandSet == null || __command == null)
            throw new NullPointerException("NARG");
        
        // Forward
        return this.request(__commandSet.id, __command.debuggerId());
    }
    
    /**
     * Creates a packet for a request.
     *
     * @param __commandSet The command set to use.
     * @param __command The command to use.
     * @return The newly created packet.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/19
     */
    public JDWPPacket request(int __commandSet, int __command)
    {
        JDWPPacket rv = this.__getPacket(true);
        
        // Use the next ID for this packet
        rv._id = this.nextId();
        
        // Packet type information
        rv._commandSet = __commandSet;
        rv._command = __command;
        rv._flags = 0;
        
        // There is no error technically
        rv._errorCode = JDWPErrorType.NO_ERROR;
        rv._rawErrorCode = 0;
        
        return rv;
    }
    
    /**
     * Sends the packet to the remote end.
     * 
     * @param __packet The packet to send.
     * @throws JDWPException If the packet could not be sent.
     * @throws NullPointerException On null arguments.
     * @since 2021/03/12
     */
    public void send(JDWPPacket __packet)
        throws JDWPException, NullPointerException
    {
        if (__packet == null)
            throw new NullPointerException("NARG");
        
        // If the handshake did not happen, do it now
        synchronized (this)
        {
            if (!this._didHandshake)
                this.__handshake();
        }
        
        // Debug
        if (JDWPCommLink.DEBUG)
            Debugging.debugNote("JDWP: -> %s", __packet);
        
        // Write to the destination
        try
        {
            // Different threads could be sending different replies, so if
            // these get mashed together then that would be a very bad thing
            // But we only need to protect the output
            synchronized (this._outMonitor)
            {
                // Write then make sure it is instantly available
                __packet.writeTo(this.out);
                this.out.flush();
            }
        }
        
        /* {@squirreljme.error AG01 Could not send the packet. (The packet)} */
        catch (IOException e)
        {
            throw new JDWPException("AG01 " + __packet, e);
        }
    }
    
    /**
     * Sets the ID sizes.
     *
     * @param __idSizes The ID sizes to set.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/22
     */
    public final void setIdSizes(JDWPIdSizes __idSizes)
        throws NullPointerException
    {
        if (__idSizes == null)
            throw new NullPointerException("NARG");
        
        synchronized (this)
        {
            this._idSizes = __idSizes;
        }
    }
    
    /**
     * Returns a fresh packet.
     * 
     * @param __open Should this be opened?
     * @return A packet that is fresh, this may be recycled from a previous
     * packet or taken from another.
     * @since 2021/03/10
     */
    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
    JDWPPacket __getPacket(boolean __open)
    {
        Deque<JDWPPacket> freePackets = this._freePackets;
        synchronized (freePackets)
        {
            // If there are no free packets then make a new one
            JDWPPacket rv;
            if (freePackets.isEmpty())
                rv = new JDWPPacket(freePackets);
            
            // Grab the next free one
            else
                rv = freePackets.remove();
            
            // Clear it for the next run
            rv.__resetAndOpen(__open, this._idSizes);
            
            return rv;
        }
    }
    
    /**
     * Performs the handshake for JDWP.
     * 
     * @throws JDWPException If the handshake could not happen.
     * @since 2021/03/08
     */
    private void __handshake()
        throws JDWPException
    {
        try
        {
            // Debug
            if (JDWPCommLink.DEBUG)
                Debugging.debugNote("JDWP: Handshake.");
            
            // The debugger sends the handshake sequence first, so as a client
            // we read from the remote end
            if (this.direction == JDWPCommLinkDirection.CLIENT_TO_DEBUGGER)
            {
                this.__handshakeRead();
                this.__handshakeWrite();
            }
            
            // Otherwise we are the debugger, so we write our handshake first
            else
            {
                this.__handshakeWrite();
                this.__handshakeRead();
            }
            
            // We did the handshake
            this._didHandshake = true;
            
            // Debug
            if (JDWPCommLink.DEBUG)
                Debugging.debugNote("JDWP: Hands shaken at a distance.");
        }
        catch (IOException e)
        {
            /* {@squirreljme.error AG04 Failed to handshake.} */
            throw new JDWPException("AG04", e);
        }
    }
    
    /**
     * Reads the handshake.
     *
     * @throws IOException On read errors.
     * @since 2024/01/19
     */
    private void __handshakeRead()
        throws IOException
    {
        // The debugger sends the handshake sequence first
        int seqLen = JDWPCommLink._HANDSHAKE_SEQUENCE.length;
        byte[] debuggerShake = new byte[seqLen];
        
        // Read in the handshake
        for (int i = 0; i < seqLen; i++)
        {
            int read = this.in.read();
            
            /* {@squirreljme.error AG02 EOF reading handshake.} */
            if (read < 0)
                throw new JDWPException("AG02");
            
            debuggerShake[i] = (byte)read;
        }
        
        /* {@squirreljme.error AG03 Debugger sent an invalid handshake.} */
        if (!Arrays.equals(debuggerShake, JDWPCommLink._HANDSHAKE_SEQUENCE))
            throw new JDWPException("AG03");
    }
    
    /**
     * Writes the handshake.
     *
     * @throws IOException On write errors.
     * @since 2024/01/19
     */
    private void __handshakeWrite()
        throws IOException
    {            
        // We then reply with our own handshake
        this.out.write(JDWPCommLink._HANDSHAKE_SEQUENCE);
        this.out.flush();
    }
}