SquirrelJME/SquirrelJME

View on GitHub
tools/squirreljme-debugger/src/main/java/cc/squirreljme/debugger/DebuggerState.java

Summary

Maintainability
B
4 hrs
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// Multi-Phasic Applications: 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.debugger;

import cc.squirreljme.jdwp.JDWPCommLink;
import cc.squirreljme.jdwp.JDWPCommandSetEventRequest;
import cc.squirreljme.jdwp.JDWPCommandSetThreadReference;
import cc.squirreljme.jdwp.JDWPCommandSetVirtualMachine;
import cc.squirreljme.jdwp.JDWPErrorType;
import cc.squirreljme.jdwp.JDWPEventKind;
import cc.squirreljme.jdwp.JDWPCommand;
import cc.squirreljme.jdwp.JDWPCommandSet;
import cc.squirreljme.jdwp.JDWPException;
import cc.squirreljme.jdwp.JDWPId;
import cc.squirreljme.jdwp.JDWPIdKind;
import cc.squirreljme.jdwp.JDWPIdSizeUnknownException;
import cc.squirreljme.jdwp.JDWPIdSizes;
import cc.squirreljme.jdwp.JDWPPacket;
import cc.squirreljme.jdwp.JDWPStepDepth;
import cc.squirreljme.jdwp.JDWPStepSize;
import cc.squirreljme.jdwp.JDWPSuspendPolicy;
import cc.squirreljme.runtime.cldc.debug.Debugging;
import cc.squirreljme.runtime.cldc.io.HexDumpOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
import net.multiphasicapps.classfile.ClassName;

/**
 * Stores the debugger state.
 *
 * @since 2024/01/19
 */
public class DebuggerState
    implements Runnable
{
    /** The communication link used. */
    protected final JDWPCommLink commLink;
    
    /** Disconnected tally. */
    protected final TallyTracker disconnectedTally =
        new TallyTracker();
    
    /** Received packets. */
    protected final TallyTracker receiveTally =
        new TallyTracker();
    
    /** Sent tally. */
    protected final TallyTracker sentTally =
        new TallyTracker();
    
    /** Waiting packets. */
    protected final TallyTracker waitingTally =
        new TallyTracker();
    
    /** Latency. */
    protected final TallyTracker latency =
        new TallyTracker();
    
    /**
     * Tally for tracking when the VM is dead.
     * 
     * @since 2024/01/27
     */
    protected final TallyTracker vmDeadTally =
        new TallyTracker();
    
    /** The current capabilities of the remote virtual machine. */
    protected final CapabilityStatus capabilities =
        new CapabilityStatus();
    
    /** Stored information manager. */
    protected final StoredInfoManager storedInfo =
        new StoredInfoManager();
    
    /** The frame context. */
    protected final ContextThreadFrame context =
        new ContextThreadFrame();
    
    /** Handlers for events. */
    protected final EventHandlers eventHandlers =
        new EventHandlers();
    
    /** Deferred packets. */
    protected final PacketDefer defer =
        new PacketDefer();
    
    /** Awaiting replies. */
    protected final AwaitingReplies replies =
        new AwaitingReplies();
    
    /** Called when the virtual machine is ready to be debugger. */
    protected final Consumer<DebuggerState> ready;
    
    /** Preferences for the debugger. */
    protected final Preferences preferences;
    
    /** Has the virtual machine been started? */
    private volatile boolean _hasStarted;
    
    /** Interpreter for byte code locations. */
    volatile FrameLocationInterpret _locationInterpret =
        FrameLocationInterpret.ADDRESS;
    
    /**
     * Initializes the debugger state.
     *
     * @param __commLink The communication link.
     * @param __preferences Debugger preferences.
     * @param __ready The callback to execute when the debugger sequence is
     * ready.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/19
     */
    public DebuggerState(JDWPCommLink __commLink, Preferences __preferences,
        Consumer<DebuggerState> __ready)
        throws NullPointerException
    {
        if (__commLink == null || __preferences == null)
            throw new NullPointerException("NARG");
        
        this.commLink = __commLink;
        this.preferences = __preferences;
        this.ready = __ready;
    }
    
    /**
     * Queries and returns all the virtual machine threads.
     *
     * @param __callback The callback to execute when the threads are obtained.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/28
     */
    public void allThreads(Consumer<InfoThread[]> __callback)
        throws NullPointerException
    {
        if (__callback == null)
            throw new NullPointerException("NARG");
        
        try (JDWPPacket out = this.request(JDWPCommandSet.VIRTUAL_MACHINE,
            JDWPCommandSetVirtualMachine.ALL_THREADS))
        {
            // Send it out
            this.send(out, (__state, __reply) -> {
                    int numThreads = __reply.readInt();
                    
                    StoredInfo<InfoThread> stored =
                        __state.storedInfo.getThreads();
                    
                    // Read in all threads
                    InfoThread[] threads = new InfoThread[numThreads];
                    for (int i = 0; i < numThreads; i++)
                    {
                        JDWPId thread = __reply.readId(JDWPIdKind.THREAD_ID);
                        threads[i] = stored.get(__state, thread);
                    }
                    
                    // Sort all the threads if we can
                    Arrays.sort(threads);
                    
                    // Send to the callback all the updated threads
                    __callback.accept(threads);
                }, ReplyHandler.IGNORED);
        }
    }
    
    /**
     * Requests that an event be listened for.
     *
     * @param __kind The kind of event to request.
     * @param __suspend The suspension policy to use for the given event.
     * @param __handler The handler for the event, this is optional.
     * @param __postResponse Handler to call when the event ID is known.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/20
     */
    public void eventSet(JDWPEventKind __kind, JDWPSuspendPolicy __suspend,
        EventModifier[] __modifiers, EventHandler<?> __handler,
        IntConsumer __postResponse)
        throws NullPointerException
    {
        if (__kind == null || __suspend == null || __handler == null)
            throw new NullPointerException("NARG");
        
        try (JDWPPacket out = this.request(JDWPCommandSet.EVENT_REQUEST,
            JDWPCommandSetEventRequest.SET))
        {
            // Fill in information
            out.writeByte(__kind.debuggerId());
            out.writeByte(__suspend.debuggerId());
            
            // No modifiers specified?
            if (__modifiers == null || __modifiers.length == 0)
                out.writeInt(0);
            
            // Write otherwise
            else
            {
                out.writeInt(__modifiers.length);
                for (EventModifier modifier : __modifiers)
                    modifier.write(this, out);
            }
            
            // Send it, wait for the response for it
            this.send(out, (__ignored, __reply) -> {
                int eventId = __reply.readInt();
                
                // Debug
                if (JDWPCommLink.DEBUG)
                    Debugging.debugNote("Awaiting Event %d -> %s",
                        eventId, __handler);
                
                // Store handler for later
                this.eventHandlers.put(eventId, __handler);
                
                // Inform of event?
                if (__postResponse != null)
                    __postResponse.accept(eventId);
            }, ReplyHandler.IGNORED);
        }
    }
    
    /**
     * Has this virtual machine been started?
     *
     * @return If it has been started or not.
     * @since 2024/01/21
     */
    public boolean hasStarted()
    {
        synchronized (this)
        {
            return this._hasStarted;
        }
    }
    
    /**
     * Looks up the given class.
     *
     * @param __className The name of the class to lookup.
     * @param __found The action to perform when the class is found.
     * @param __notFound The action to perform when the class was not found.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/22
     */
    public void lookupClass(ClassName __className,
        Consumer<InfoClass[]> __found, Consumer<Throwable> __notFound)
        throws NullPointerException
    {
        if (__className == null)
            throw new NullPointerException("NARG");
        
        try (JDWPPacket packet = this.request(JDWPCommandSet.VIRTUAL_MACHINE,
            JDWPCommandSetVirtualMachine.CLASSES_BY_SIGNATURE))
        {
            // Request the class
            packet.writeString(__className.field().toString());
            
            // Send it off and wait for a response before doing something
            this.send(packet,
                (__state, __reply) ->
                {
                    // Timed out?
                    if (__reply == null)
                    {
                        __notFound.accept(new Throwable("Timed out."));
                        return;
                    }
                    
                    // Was there an error?
                    if (__reply.hasError())
                    {
                        __notFound.accept(new Throwable(
                            __reply.error().toString()));
                        return;
                    }
                    
                    // Were there any resultant classes?
                    int count = __reply.readInt();
                    if (count == 0)
                    {
                        __notFound.accept(new Throwable("No classes found."));
                        return;
                    }
                    
                    // Get references for each class
                    StoredInfo<InfoClass> classStorage =
                        __state.storedInfo.getClasses();
                    
                    // Read all class IDs
                    InfoClass[] foundClasses = new InfoClass[count];
                    for (int i = 0; i < count; i++)
                    {
                        // Ignore tag
                        __reply.readByte();
                        
                        // Get referenced class
                        foundClasses[i] = classStorage.get(__state,
                            __reply.readId(JDWPIdKind.REFERENCE_TYPE_ID));
                        
                        // Ignore status
                        __reply.readInt();
                    }
                    
                    // Call the handler with all the found classes
                    __found.accept(foundClasses);
                }, ReplyHandler.IGNORED);
        }
    }
    
    /**
     * Reads the location information.
     *
     * @param __inThread The thread this is in.
     * @param __packet The packet to read the location information from.
     * @return The resultant location.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/25
     */
    public FrameLocation readLocation(InfoThread __inThread,
        JDWPPacket __packet)
        throws NullPointerException
    {
        if (__inThread == null || __packet == null)
            throw new NullPointerException("NARG");
        
        // Ignore tag
        __packet.readByte();
        
        // Get the class and method we are in
        JDWPId classId = __packet.readId(JDWPIdKind.REFERENCE_TYPE_ID);
        JDWPId methodId = __packet.readId(JDWPIdKind.METHOD_ID);
        
        // Read location index
        long index = __packet.readLong();
        
        StoredInfoManager storedInfo = this.storedInfo;
        InfoClass inClass = storedInfo.getClasses().get(this, classId);
        
        // Initialize location
        return new FrameLocation(
            __inThread,
            inClass,
            inClass.getMethod(methodId),
            index);
    }
    
    /**
     * Creates a reply packet.
     *
     * @param __id The 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/21
     */
    public JDWPPacket reply(JDWPPacket __id, JDWPErrorType __error)
        throws NullPointerException
    {
        if (__id == null || __error == null)
            throw new NullPointerException("NARG");
        
        return this.reply(__id.id(), __error);
    }
    
    /**
     * Creates a reply packet.
     *
     * @param __id The 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/21
     */
    public JDWPPacket reply(int __id, JDWPErrorType __error)
        throws NullPointerException
    {
        if (__error == null)
            throw new NullPointerException("NARG");
        
        return this.commLink.reply(__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/20
     */
    public JDWPPacket request(JDWPCommandSet __commandSet,
        JDWPCommand __command)
        throws NullPointerException
    {
        return this.commLink.request(__commandSet, __command);
    }
    
    /**
     * 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/20
     */
    public JDWPPacket request(int __commandSet, int __command)
    {
        return this.commLink.request(__commandSet, __command);
    }
    
    /**
     * {@inheritDoc}
     * @since 2024/01/19
     */
    @Override
    public void run()
    {
        JDWPCommLink link = this.commLink;
        TallyTracker receiveTally = this.receiveTally;
        PacketDefer defer = this.defer;
        
        // Perform default trackers
        this.__defaultInit();
        
        // Infinite read loop, read in packets accordingly
        boolean sizesKnown = false;
        for (;;)
        {
            // Debug
            if (JDWPCommLink.DEBUG)
                Debugging.debugNote("Polling JDWPPacket...");
            
            // Do we know sizes yet?
            if (!sizesKnown)
            {
                // Process any deferred packets if they are known
                sizesKnown = link.areSizesKnown();
                if (sizesKnown)
                {
                    // Debug
                    if (JDWPCommLink.DEBUG)
                        Debugging.debugNote(
                            "Processing deferred packets...");
                    
                    // These packets are from a time when sizes are not known,
                    // so we need to place in all the sizes
                    JDWPIdSizes sizes = link.idSizes();
                    
                    // Read in everything so we do not keep a lock
                    JDWPPacket[] deferred = defer.removeAll();
                    
                    // Process all the packets
                    if (deferred != null)
                        for (JDWPPacket copy : deferred)
                            try (JDWPPacket packet = copy)
                            {
                                // Set sizes
                                packet.setIdSizes(sizes);
                                
                                // Process it
                                this.__process(packet.resetReadPosition());
                            }
                    
                    // Call the ready handler if one is set
                    if (this.ready != null)
                        this.ready.accept(this);
                }
            }
            
            // Poll next packet
            try (JDWPPacket packet = link.poll())
            {
                // Debug
                if (JDWPCommLink.DEBUG)
                {
                    Debugging.debugNote("DEBUGGER <- %s", packet);
                    
                    if (packet != null)
                        try (HexDumpOutputStream dump =
                                 new HexDumpOutputStream(System.err))
                        {
                            dump.write(packet.toByteArray());
                        }
                        catch (IOException ignored)
                        {
                        }
                }
                
                // Only when interrupted or terminated does this stop
                if (packet == null)
                {
                    if (JDWPCommLink.DEBUG)
                        Debugging.debugNote(
                            "JDWP Interrupted/Terminated.");
                    
                    break;
                }
                
                // Tally up!
                receiveTally.increment();
                
                // Perform processing on it
                this.__process(packet);
            }
            catch (JDWPException __e)
            {
                // Print error to the output
                __e.printStackTrace(System.err);
                
                // Stop if shutdown
                if (link.isShutdown())
                    break;
            }
        }
        
        // Debug
        if (JDWPCommLink.DEBUG)
            Debugging.debugNote("JDWP Loop End...");
        
        // Disconnected so indicate that
        this.disconnectedTally.increment();
    }
    
    /**
     * Sends the given packet.
     *
     * @param __packet The packet to send.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/19
     */
    public void send(JDWPPacket __packet)
        throws NullPointerException
    {
        this.send(__packet, null, null);
    }
    
    /**
     * Sends the given packet with an optional reply handler for when a
     * response is received.
     *
     * @param __packet The packet to send.
     * @param __pass The reply handler to use for this packet.
     * @param __fail Called when the packet has failed.
     * @throws IllegalArgumentException If a reply handler is specified and
     * this is a reply packet.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/19
     */
    public void send(JDWPPacket __packet, ReplyHandler __pass,
        ReplyHandler __fail)
        throws IllegalArgumentException, NullPointerException
    {
        this.send(__packet, __pass, __fail, null);
    }
    
    /**
     * Sends the given packet with an optional reply handler for when a
     * response is received.
     *
     * @param __packet The packet to send.
     * @param __pass The reply handler to use for this packet.
     * @param __fail Called when the packet has failed.
     * @param __always This handler is always called.
     * @throws IllegalArgumentException If a reply handler is specified and
     * this is a reply packet.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/27
     */
    public void send(JDWPPacket __packet, ReplyHandler __pass,
        ReplyHandler __fail, ReplyHandler __always)
        throws IllegalArgumentException, NullPointerException
    {
        if (__packet == null)
            throw new NullPointerException("NARG");
        
        // Wanting to handle a reply to this packet 
        if (__pass != null)
        {
            // It makes no sense to handle a reply to a reply
            if (__packet.isReply())
                throw new IllegalArgumentException(
                    "Cannot handle a reply to a reply.");
            
            // Store handler for replies before we send as the pipe could be
            // really fast!
            this.replies.await(__packet.id(), __pass, __fail, __always);
            
            // Mark as waiting
            this.waitingTally.increment();
        }
        
        // Debug
        if (JDWPCommLink.DEBUG)
            Debugging.debugNote("DEBUGGER -> %s (%x)",
                __packet, System.identityHashCode(__packet));
        
        // Send over the link
        this.commLink.send(__packet);
        
        // Tally up
        this.sentTally.increment();
    }
    
    /**
     * Helper for sending known values that always calls sync at the end.
     *
     * @param __out The out packet.
     * @param __value The known value.
     * @param __sync The sync to call.
     * @param __pass Called on successful packets.
     * @param __fail Called on failed packets.
     * @since 2024/01/27
     */
    public void sendKnown(JDWPPacket __out, KnownValue<?> __value,
        KnownValueCallback<?> __sync, ReplyHandler __pass,
        ReplyHandler __fail)
    {
        this.send(__out, __pass, __fail, (__state, __reply) -> {
                if (__sync != null)
                    __sync.sync(__state, (KnownValue)__value);
            });
    }
    
    /**
     * Sets that the virtual machine has started.
     *
     * @since 2024/01/21
     */
    public void setStarted()
    {
        synchronized (this)
        {
            this._hasStarted = true;
        }
    }
    
    /**
     * Resumes a single thread.
     *
     * @param __thread The thread to resume.
     * @param __done The method to call when the action has been performed.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/21
     */
    public void threadResume(InfoThread __thread, Runnable __done)
        throws NullPointerException
    {
        if (__thread == null)
            throw new NullPointerException("NARG");
        
        try (JDWPPacket out = this.request(JDWPCommandSet.THREAD_REFERENCE,
            JDWPCommandSetThreadReference.RESUME))
        {
            // Write the ID of the thread
            out.writeId(__thread.id);
            
            // Send it
            this.send(out, (__ignored, __reply) -> {
                // Inform that it was done
                if (__done != null)
                    __done.run();
            }, ReplyHandler.IGNORED);
        }
    }
    
    /**
     * Resumes all threads.
     *
     * @param __done The method to call when the action has been performed.
     * @since 2024/01/21
     */
    public void threadResumeAll(Runnable __done)
    {
        // The all version uses the VM command set as there is no
        // base thread to use
        try (JDWPPacket out = this.request(JDWPCommandSet.VIRTUAL_MACHINE,
            JDWPCommandSetVirtualMachine.RESUME))
        {
            // Send it
            this.send(out, (__ignored, __reply) -> {
                if (__done != null)
                    __done.run();
            }, ReplyHandler.IGNORED);
        }
    }
    
    /**
     * Steps in a thread.
     *
     * @param __thread The thread to step in.
     * @param __count The steps to make.
     * @param __depth The step depth.
     * @param __size The step size.
     * @param __handler The handler to use for the event update.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/26
     */
    public void threadStep(InfoThread __thread, int __count,
        JDWPStepDepth __depth, JDWPStepSize __size,
        EventHandler<SingleStepEvent> __handler)
        throws NullPointerException
    {
        if (__thread == null || __depth == null || __size == null)
            throw new NullPointerException("NARG");
        
        // Modifiers to use
        EventModifier[] modifiers = {
                new EventModifierCount(__count),
                new EventModifierSingleStep(__thread,
                    __depth, __size)
            };
        
        // An example single step
        // EventRequest[id=483,kind=SINGLE_STEP,suspend=ALL,left=1,
        // filter=EventFilter{callStackStepping=CallStackStepping[
        // thread=Thread-6: callback#0,depth=0,size=1],
        // exception=null, excludeClass=ClassPattern(kotlin.
        // KotlinNullPointerException), fieldOnly=null,
        // includeClass=null, location=null, thisInstance=null,
        // thread=null, type=null}]
        // Compared to:
        // EventRequest[id=125,kind=SINGLE_STEP,suspend=ALL,left=1,
        // filter=EventFilter{callStackStepping=CallStackStepping[
        // thread=Thread-1: main,depth=OUT,size=MIN], exception=null, 
        // excludeClass=null, fieldOnly=null, includeClass=null, 
        // location=null, thisInstance=null, thread=null, type=null}]
        this.eventSet(JDWPEventKind.SINGLE_STEP,
            JDWPSuspendPolicy.EVENT_THREAD,
            modifiers,
            __handler,
            (__id) -> {
                // The thread is stopped, so we must resume here
                this.threadResume(__thread, null);
            });
    }
    
    /**
     * Suspends the specified thread.
     *
     * @param __thread The thread to suspend.
     * @param __done The method to call when the action has been performed.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/26
     */
    public void threadSuspend(InfoThread __thread, Runnable __done)
        throws NullPointerException
    {
        if (__thread == null)
            throw new NullPointerException("NARG");
        
        try (JDWPPacket out = this.request(JDWPCommandSet.THREAD_REFERENCE,
            JDWPCommandSetThreadReference.SUSPEND))
        {
            // Write the ID of the thread
            out.writeId(__thread.id);
            
            // Send it
            this.send(out, (__ignored, __reply) -> {
                if (__done != null)
                    __done.run();
            }, ReplyHandler.IGNORED);
        }
    }
    
    /**
     * Suspends all threads.
     *
     * @param __done The method to call when the action has been performed.
     * @since 2024/01/25
     */
    public void threadSuspendAll(Runnable __done)
    {
        // The all version uses the VM command set as there is no
        // base thread to use
        try (JDWPPacket out = this.request(JDWPCommandSet.VIRTUAL_MACHINE,
            JDWPCommandSetVirtualMachine.SUSPEND))
        {
            // Send it
            this.send(out, (__ignored, __reply) -> {
                if (__done != null)
                    __done.run();
            }, ReplyHandler.IGNORED);
        }
    }
    
    /**
     * Default initialization.
     *
     * @since 2024/01/20
     */
    private void __defaultInit()
    {
        // We need to know the sizes of variable length entries, otherwise
        // we cannot read them at all
        try (JDWPPacket packet = this.request(JDWPCommandSet.VIRTUAL_MACHINE,
            JDWPCommandSetVirtualMachine.ID_SIZES))
        {
            this.send(packet,
                (__state, __reply) -> {
                    Debugging.debugNote("Read ID Sizes...");
                    
                    // Read all of these in
                    int[] sizes = new int[JDWPIdKind.NUM_KINDS];
                    for (int i = 0; i < sizes.length; i++)
                        sizes[i] = __reply.readInt();
                    
                    // Initialize sizes
                    JDWPIdSizes result = new JDWPIdSizes(sizes);
                    this.commLink.setIdSizes(result);
                    
                    // Debug
                    Debugging.debugNote("Sizes read: %s",
                        result);
                }, ReplyHandler.IGNORED);
        }
        
        // Version information
        try (JDWPPacket packet = this.request(JDWPCommandSet.VIRTUAL_MACHINE,
            JDWPCommandSetVirtualMachine.VERSION))
        {
            this.send(packet, this::__remoteVmInfo, ReplyHandler.IGNORED);
        }
        
        // Get the capabilities of the remote VM, so we know what we can and
        // cannot do
        this.capabilities.update(this);
        
        // Thread events, with no particular handler
        this.eventSet(JDWPEventKind.THREAD_START, JDWPSuspendPolicy.NONE,
            null, (__state, __event) -> {
            }, null);
        this.eventSet(JDWPEventKind.THREAD_DEATH, JDWPSuspendPolicy.NONE,
            null, (__state, __reply) -> {}, null);
        
        // Trick the SquirrelJME hosted environment to handle
        // DebugShelf.breakpoint() calls
        this.eventSet(JDWPEventKind.EXCEPTION, JDWPSuspendPolicy.ALL,
            new EventModifier[]{new EventModifierClassMatch(
                "cc.squirreljme.emulator.__PseudoBreakpoint__")},
            (__state, __reply) -> {}, null);
    }
    
    /**
     * Processes the given packet, or defers it.
     *
     * @param __packet The packet to process.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/23
     */
    private void __process(JDWPPacket __packet)
        throws NullPointerException
    {
        if (__packet == null)
            throw new NullPointerException("NARG");
        
        JDWPCommLink link = this.commLink;
        PacketDefer defer = this.defer;
                
        // Try to process the packet
        try
        {
            // Handle packet
            if (__packet.isReply())
                this.__processReply(__packet);
            else
                this.__processRequest(__packet);
        }
        
        // The sizes are not known, we still want this packet, but we
        // need to handle it at some point
        catch (JDWPIdSizeUnknownException ignored)
        {
            defer.defer(link, __packet);
        }
        
        // General exception
        catch (JDWPException __e)
        {
            __e.printStackTrace();
            
            throw __e;
        }
    }
    
    /**
     * Processes the given request packet.
     *
     * @param __packet The packet to process.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/19
     */
    private void __processReply(JDWPPacket __packet)
        throws NullPointerException
    {
        if (__packet == null)
            throw new NullPointerException("NARG");
        
        // See if there is a handler for this reply
        AwaitingReply handler = this.replies.remove(__packet.id());
        
        // If there is no handler, just ignore
        if (handler == null)
        {
            if (JDWPCommLink.DEBUG)
                Debugging.debugNote("No handler for reply %d...",
                    __packet.id());
            
            return;
        }
        
        // Set latency
        int latency = (int)Math.min(Integer.MAX_VALUE,
            (System.nanoTime() - handler.nanoTime) / 1_000_000L);
        this.latency.set(latency);
        
        // Debug
        if (JDWPCommLink.DEBUG)
            Debugging.debugNote("Packet %d (%08x): Latency %d ms",
                handler.id, handler.id, latency);
        
        // Decrement waiting count
        this.waitingTally.decrement();
        
        // Always ensure that the always-callback gets executed regardless
        // of any exceptions or otherwise that may occur
        try
        {
            // Call the pass/fail handler accordingly
            if (__packet.hasError())
            {
                if (handler.fail != null)
                    handler.fail.handlePacket(this, __packet);
            }
            else
            {
                if (handler.pass != null)
                    handler.pass.handlePacket(this, __packet);
            }
        }
        finally
        {
            // Call the always-handler?
            if (handler.always != null)
                handler.always.handlePacket(this, __packet);
        }
    }
    
    /**
     * Processes the given request packet.
     *
     * @param __packet The packet to process.
     * @throws NullPointerException On null arguments.
     * @since 2024/01/19
     */
    private void __processRequest(JDWPPacket __packet)
        throws NullPointerException
    {
        if (__packet == null)
            throw new NullPointerException("NARG");
        
        // If this is an event, handle it specifically
        if (__packet.commandSetId() == 64 && __packet.command() == 100)
        {
            // Debug
            if (JDWPCommLink.DEBUG)
                Debugging.debugNote("Process event: %s", __packet);
            
            // Handle this as an event
            EventProcessor.handle(this, __packet);
            
            // Do not do any more processing
            return;
        }
        
        // Debug
        if (JDWPCommLink.DEBUG)
            Debugging.debugNote("Handle non-event?");
    }
    
    /**
     * Initializes the remote virtual machine info.
     *
     * @param __state The state used.
     * @param __reply The reply packet.
     * @since 2024/01/26
     */
    private void __remoteVmInfo(DebuggerState __state, JDWPPacket __reply)
    {
        // Read all the info
        String desc = __reply.readString();
        int jdwpMajor = __reply.readInt();
        int jdwpMinor = __reply.readInt();
        String vmVersion = __reply.readString();
        String vmName = __reply.readString();
        
        // SquirrelJME uses indexes for byte code instructions rather than
        // their actual address
        if (desc.contains("SquirrelJME") || vmName.contains("SquirrelJME"))
            this._locationInterpret = FrameLocationInterpret.INDEX;
        
        // Note it
        Debugging.debugNote("Description: %s",
            desc);
        Debugging.debugNote("JDWP Major: %d",
            jdwpMajor);
        Debugging.debugNote("JDWP Minor: %d",
            jdwpMinor);
        Debugging.debugNote("VM Version: %s",
            vmVersion);
        Debugging.debugNote("VM Name: %s",
            vmName);
    }
}