# Notice.
#  This back-end Python debugger for jGRASP is under development. Although the license below is restrictive, this is
#  only temporary. When it becomes more complete and efficient, we anticipate releasing it under a public license,
#  as a machine-interface debugger back end separate from jGRASP, and possibly also with a command line interface
#  added.
#
# Copyright (c) 2026 Auburn University (jGRASP Group). All Rights Reserved.
#
# Software License:
#
# Section 1. License Grant.
#  Auburn University grants to you a non-exclusive and non-transferable license to use the jGRASP Python Debugger. The
#  jGRASP Python Debugger may not be installed except as part of a jGRASP installation. jGRASP is a Trademark of
#  Auburn University.
#
# Section 2. Restrictions.
#  The jGRASP Python Debugger is proprietary and confidential. It may not be used, copied, modified, or distributed
#  without the express written permission of the jGRASP Group at Auburn University. Unauthorized use, reproduction, or
#  distribution separate from jGRASP is strictly prohibited.
#
# Section 3. Disclaimer of Warranty. 
#  The jGRASP Python Debugger is licensed "as is". There are no express or implied warranties, including, but not
#  limited to, the implied warranties of merchantability and fitness for a particular purpose. Auburn University makes
#  no warranty with respect to the accuracy or completeness of information obtained through the use of this program.
#  Auburn University does not warrant that the jGRASP Python Debugger will meet all of your requirements or that its
#  operation will be uninterrupted or error free or that any defect within the jGRASP Python Debugger will be
#  corrected. No oral or written information, representation, or advice given by Auburn University or an authorized
#  representative of Auburn University shall create a warranty. Auburn University and its agents shall in no event be
#  held liable to the user for any damages, including direct, indirect, incidental, or consequential damages, lost
#  profits, lost savings, or other such damages arising out of the installation, use, improper use, or inability to
#  use the jGRASP Python Debugger, even if Auburn University has been advised of the possibility of such damages, or
#  any claim by any other person or entity related thereto.

from __future__ import annotations
import argparse
import builtins
import types
from collections import deque
from enum import Enum, IntFlag
from pathlib import Path
import typing
from typing import Set, List, Dict, Deque, Tuple, Any, Iterable, Iterator, Never, cast
import re
import sys
import socket
import threading
import time
import inspect
from inspect import FrameInfo
import queue
import traceback
import weakref
from weakref import ReferenceType
# import faulthandler
from itertools import islice
import io
import ast
import os
import faulthandler
      

# some aliases
mon = sys.monitoring
E = mon.events
debugger_file: str = __file__
if debugger_file.endswith(".pyc"):
   debugger_file = debugger_file[:-1]

class StepMode(Enum):
   """ Stepping mode """
   over = 0
   """ Step over lines """
   into = 1
   """ Step and step into functions """
   out = 2
   """ Step out of the current function """


class ValueData:
   """ Data for tracked values in the debugger. """
   
   def __init__(self, iid: int, ref_count: int) -> None:
      self.ref_count = ref_count
      self.iid = iid


class BPData:
   """ Breakpoint data. """
   def __init__(self, line: int):
      self.line = line


class RuntoData:
   """ Data for a run-to. """
   def __init__(self, file: str, line: int):
      self.file = file
      self.line = line


class FileData:
   """ Per-file data. """
   
   _exec_path = None
   """ Python executable base path for step-in exclusions. """


   def __init__(self, path: str) -> None:
      """ Creates a FileData.
   
   :param str path: the real path for the file. """
      self.path: str = path
      """ The real path for the file. """
      self._lines: Dict[int, Set[int]] = {}
      """ Map from lines to code object ids that contain the line. """
      self.breakpoints: Dict[int, Set[BPData]] = {}
      """ Map from lines containing breakpoints to breakpoint data. """
      # Note that path == debugger_file should only happen at the breakpoint hook entry. Everywhere else should be guarded
      # by JGraspPythonDebugger.threadLocal.inDebug .
      self.always_ignore = path == debugger_file
      """ True if we should never stop in this file, false otherwise. """
      self.ignore = self.always_ignore or (path.startswith('<') and not path == "<interactions>")\
            or FileData.is_python_path(path)
      """ True if no breakpoints or stepping are allowed in this file unless forced, false otherwise. """
      self._coi_to_code: Dict[int, ReferenceType[types.CodeType]] = {}
      """ Map from code object ids to weak references of code objects. """


   def add_code(self, code: types.CodeType, code_id: int, dbg: JGraspPythonDebugger) -> None:
      """ Adds per-file data for a newly encountered code object.
   
   :param types.CodeType code: the new code object.
   
   :param int code_id: the id of the new code object.
   
   :param JGraspPythonDebugger dbg: the debugger. """
      self._coi_to_code[code_id] = weakref.ref(code)
      new_lines: Set[int] = set()
      sle: bool = dbg.stop_at_entry and code_id == dbg.initial_code_object_id
      bps_found: bool = False
      with dbg.code_ids_events_lock:
         for s,e,ln in code.co_lines():
            if ln is not None:
               lines: Set[int] = self._lines.setdefault(ln, set())
               if not lines:
                  new_lines.add(ln)
               lines.add(code_id)
               bps: (Set[BPData] | None) = self.breakpoints.get(ln)
               if bps is not None:
                  bps_found = True
                  events: EventCounts = dbg.code_ids_events.setdefault(code_id, EventCounts())
                  was_zero: bool = events.line == 0
                  events.line += len(bps)
                  sle |= was_zero != (events.line == 0)
         if sle:
            if not bps_found: # Otherwise already initialized.
               dbg.code_ids_events.setdefault(code_id, EventCounts())
            dbg.set_local_events(code, code_id)
         if sle or bps_found: 
            mon.restart_events()
   
      if new_lines:
         dbg.write_event(f"valid#{self.path}#{"|".join(str(ln) for ln in new_lines)}\n")
    
    
   def add_breakpoint(self, line: int, dbg: JGraspPythonDebugger) -> None:
      """ Adds a breakpoint.
   
   :param int line: the line on which the breakpoint will be added.
   
   :param JGraspPythonDebugger dbg: the debugger. """
      bps: Set[BPData] = self.breakpoints.setdefault(line, set())
      bps.add(BPData(line))
      self.change_breakpoint_count(line, 1, dbg)
      
      
   def add_runto(self, line: int, dbg: JGraspPythonDebugger) -> None:
      """ Adds a run-to-cursor line.
   
   :param int line: the line on which debugging should stop.
   
   :param JGraspPythonDebugger dbg: the debugger. """
      self.change_breakpoint_count(line, 1, dbg)
      
      
   def remove_runto(self, line: int, dbg: JGraspPythonDebugger) -> None:
      """ Removes a run-to-cursor line.
   
   :param int line: the line on which debugging should no longer stop.
   
   :param JGraspPythonDebugger dbg: the debugger. """
      self.change_breakpoint_count(line, -1, dbg)
      
      
   def change_breakpoint_count(self, line: int, count: int, dbg: JGraspPythonDebugger) -> None:
      """ Changes the breakpoint count for all code ids at a specified line.
   
   :param int line: the line on which the counts will be changed.
   
   :param int count: the amount by which the counts will be changed.
   
   :param JGraspPythonDebugger dbg: the debugger. """
      code_ids = self._lines.get(line)
      if code_ids is None:
         return
      for code_id in code_ids:
         with dbg.code_ids_events_lock:
            events: EventCounts = dbg.code_ids_events.setdefault(code_id, EventCounts())
            was_zero: bool = events.line == 0
            events.line += count
            if was_zero != (events.line == 0):
               # Set or remove line events for code object.
               code_ref: ReferenceType[types.CodeType] = self._coi_to_code[code_id]
               if code_ref is not None:
                  code: (types.CodeType | None) = code_ref()
                  if code is not None:
                     dbg.set_local_events(code, code_id)
      if count > 0:
         mon.restart_events()
    
    
   def add_breakpoints(self, lines: List[int], dbg: JGraspPythonDebugger) -> None:
      """ Adds breakpoints.
   
   :param List[int] lines: the lines on which the breakpoints will be added.
   
   :param JGraspPythonDebugger dbg: the debugger. """
      for line in lines:
         bps: Set[BPData] = self.breakpoints.setdefault(line, set())
         bps.add(BPData(line))
         code_ids = self._lines.get(line)
         if code_ids is None:
            continue
         for code_id in code_ids:
            with dbg.code_ids_events_lock:
               events: EventCounts = dbg.code_ids_events.setdefault(code_id, EventCounts())
               events.line += 1
               if events.line == 1:
                  # Set line events if not already set.
                  code_ref: ReferenceType[types.CodeType] = self._coi_to_code[code_id]
                  if code_ref is not None:
                     code: (types.CodeType | None) = code_ref()
                     if code is not None:
                        dbg.set_local_events(code, code_id)
      if len(lines) > 0:
         mon.restart_events()
    
    
   def remove_breakpoint(self, line: int, dbg: JGraspPythonDebugger) -> None:
      """ Removes a breakpoint or all breakpoints for the file.
   
   :param int line: the line on which the breakpoint will be removed. If this is -1, all breakpoints for the file\
   will be removed.
   
   :param JGraspPythonDebugger dbg: the debugger. """
      if line == -1: # Remove all.
         ln: int
         bps: Set[BPData]
         for ln, bps in self.breakpoints.items():
            self.change_breakpoint_count(ln, -len(bps), dbg)
         self.breakpoints = {}
      else:
         count: int = len(self.breakpoints[line])
         del self.breakpoints[line]
         if count > 0:
            self.change_breakpoint_count(line, -count, dbg)


   @staticmethod
   def is_python_path(path: str) -> bool:
      """ Determines if a path is within the Python distribution.
   
   :param str path: the path.
   
   :return: True if the Python distribution is locatable and the file is within that path, False otherwise.
   :rtype: bool """
   
      if FileData._exec_path is None:
         if sys.executable is None:
            FileData._exec_path = ""
         else:
            result: str = os.path.dirname(os.path.realpath(sys.executable))
            head, tail = os.path.split(result)
            if tail == "bin":
               FileData._exec_path = head + os.path.sep
            else:
               FileData._exec_path = result + os.path.sep
      
      return FileData._exec_path != "" and path.startswith(FileData._exec_path)


class StepData:
   """ In-process step data. """
   
   def __init__(self, mode: StepMode, forced: bool, depth: int, byte: bool, skip_first: bool,
         stack: List[FrameInfo]) -> None:
      """ Creates a StepData.
   
   :param StepMode mode: the step type.
   
   :param bool forced: true if the current step should ignore step excludes, false if it should not.
   
   :param int depth: the initial frame depth when stepping.
   
   :param byte: true if the current step is a byte code step, false otherwise.
   
   :param skip_first: true if the first step target should be skipped, false otherwise.
   
   :param List[FrameInfo] stack: the stack of the caller. """
      self.mode: StepMode = mode
      """ The current step mode. """
      self.wait_for_out: bool = mode is StepMode.out
      """ True for a step-out until the thread is out. Once out, a step-out essentially becomes a step-over. """
      self.forced: bool = forced
      """ True if the current step should ignore step excludes, false if it should not. """
      self.byte: bool = byte
      """ True if the current step is a byte code step, false otherwise. """
      self.skip_first: bool = skip_first
      """ True if the first instruction stop should be ignored (needed for code breakpoints and byte code steps). """
      self.depth: int = depth
      """ The current frame depth when stepping. """
      self.code: types.CodeType = stack[len(stack) - depth + 1].frame.f_code
      """ Code block in which the step began. """
      self.code_id: int = id(self.code)
      """ Id of the code block in which the step began. """
      self.out: bool = False
      """ True if this is a return or yield step out, false otherwise. """


   def set_code(self, depth: int, stack: List[FrameInfo], out : bool) -> None:
      """ Resets the depth and code block.
   
   :param int depth: the initial frame depth when stepping.
   
   :param List[FrameInfo] stack: the stack of the caller.
   
   :param bool out: true if this is a return or yield step out, false otherwise. """
      self.depth = depth
      self.out = out
      self.code = stack[len(stack) - depth + 1].frame.f_code
      self.code_id = id(self.code)


class ThreadFlag(IntFlag):
   """ Permanent thread data flags. """
   
   WORKBENCH = 1
   """ Indicates that the thread is the main thread in a workbench or interactions session. """


class ThreadStateFlag(IntFlag):
   """ Changeable thread data flags. """
   
   ALIVE = 1
   """ Indicates that the thread is alive. """
   
   DAEMON = 2
   """ Indicates that the thread is a daemon thread. """
   
   RUN_TO_CURSOR = 4
   """ Indicates that the thread is running to cursor. """
   
   STEPPING = 8
   """ Indicates that the thread is stepping. """


class ThreadData:
   """ Per-thread data. """

   def __init__(self, thread_id: int, t: threading.Thread, isWorkbench: bool) -> None:
      """ Creates a FileData.
   
   :param int thread_id: the thread id.
   
   :param threading.Thread t: the thread.
   
   :param bool isWorkbench: true if this thread is in a workbench/interactions session, false if it is not. """
      self.thread_id = thread_id
      """ The thread id. This will not change. """
      self.name = t.name
      """ The thread name. This is updated only when state_data() is called. """
      self.stopped: bool = False 
      """ True if the thread is stopped. """
      self.cmd_queue: queue.Queue = queue.Queue()
      """ Debug command processing queue for stopped threads. """
      fixed_flags: int = 0
      if isWorkbench and t is threading.main_thread():
         fixed_flags |= ThreadFlag.WORKBENCH
      self.stable_data: str = f"{thread_id}|{t.native_id}|{fixed_flags}"
      """ A string representation of the unchanging data. """
      self.previous_state_data: str = ""
      """ The state data the last time state_data() was called, empty if it has not been called. """
      self.executing_step: (StepData | None) = None
      """ Data for the currently executing step, or None if no step is executing. """
      self.runto: (RuntoData | None) = None
      """ Data for the current run-to-cursor location, or None if no run-to-cursor is in effect. """
     
     
   def state_data(self, t: threading.Thread) -> str:
      """ Gets current state data for the thread.
   
   :return: The thread name and alive state, separated by a bar.
   :rtype: str """
      self.name = t.name
      flags: int = 0
      if t.is_alive():
         flags |= ThreadStateFlag.ALIVE
      if t.daemon:
         flags |= ThreadStateFlag.DAEMON
      if self.runto is not None:
         flags |= ThreadStateFlag.RUN_TO_CURSOR
      if self.executing_step is not None:
         flags |= ThreadStateFlag.STEPPING
      self.previous_state_data = f"{self.name}|{flags}"
      return self.previous_state_data
     
     
   def full_data(self, t: threading.Thread) -> str:
      """ Gets current permanent and state data for the thread.
   
   :param threading.Thread t: the thread.
   
   :return: The thread id, native_id, daemon state, name, and alive state, separated by bars.
   :rtype: str """
      return f"{self.stable_data}|{self.state_data(t)}"


class EventCounts:
   """ Event request counts for a code object. """
   
   def __init__(self) -> None:
      """ Creates an EventCounts. """
      
      self.line: int = 0
      """ Number of line requests. """
      
      self.instruction: int = 0
      """ Number of instruction requests. """
      
      self.retrn: int = 0
      """ Number of return requests. """
      
      
   def to_events(self, stop_at_entry: bool) -> int:
      """ Creates a local events bit flags set based on the requests.
   
   :param stop_at_entry: true if the debugger is waiting for the first line to stop at entry, false if it is not.
   
   :return: The events bit flags set.
   :rtype: int """
   
      result: int = 0
      if stop_at_entry or self.line > 0:
         result += E.LINE
      if self.instruction > 0:
         result += E.INSTRUCTION
      if self.retrn > 0:
         result += E.PY_RETURN + E.PY_YIELD
      return result


class JGraspPythonDebugger:
   """ CPython-based debugger for Python 3.12 and higher. """

   ATTRIBUTES: int = 2
   """ Flag for retrieving attributes. """
   
   ELEMENTS: int = 4
   """ Flag for retrieving elements. """

   filterTypes: Set[type] = {
         staticmethod,
         type,
         types.BuiltinFunctionType,
         types.BuiltinMethodType,
         types.ClassMethodDescriptorType,
         types.GetSetDescriptorType,
         types.MemberDescriptorType,
         types.MethodDescriptorType,
         types.ModuleType,
         types.FunctionType,
         types.WrapperDescriptorType,
         typing._SpecialGenericAlias
         }
   """ Types for which variables will not be shown when filtering is on. """
   
   encodeTable: dict = str.maketrans({"\\" : r"\\",  "\n": r"\n",  "\r": r"\r",  "#": r"\h",  "%": r"\p",  "|": r"\b" })
   """ Response element encoding table. """      


   def __init__(self, sock: socket.socket, isWorkbench: bool) -> None:
      """ Creates a JGraspPythonDebugger.
   
   :param socket.socket sock: the communications socket.
   
   :param bool isWorkbench: true if this is a workbench or viewer session, false if it is not. """
      super().__init__()
      self._sock = sock
      """ The comms socket. """
      self.isWorkbench = isWorkbench
      """ True if this is a workbench or viewer session, false if it is not. """ 
      self.byte_code_steps = False
      """ True if stepping should use VM byte code size steps, false if it should use source size steps. """
      self.stop_at_breakpoints_while_stepping = False
      """ True if the debugger should stop at breakpoints mid-step, false if it should not. """
      self.code_ids_events: Dict[int, EventCounts] = {}
      """ Local monitor event counts for each code id. """
      self.code_ids_events_lock: threading.Lock = threading.Lock()
      """ Lock for code id events. """
      self._is_first_line_event = True
      """ True if the first line event for the debugger has not yet happened, false if it has. """
      self._tool_id = mon.DEBUGGER_ID
      """ The tool id. """
      self.stop_at_entry = False
      """ True if the debugger should stop at the main program entry point, false if it should not. """
      self._coi_to_file_data: Dict[int, FileData] = {}
      """ Map from code object ids to file data. """
      self.fn_to_file_data: Dict[str, FileData] = {}
      """ Map from filename to per-file data. Multiple paths representing the same file may map to the same data, but
   if so at least one will be the real path. """
      self.file_data_lock: threading.Lock = threading.Lock()
      """ Lock for per-file data. """
      self.thread_id_to_data: Dict[int, ThreadData] = {}
      """ Map from thread id to per-thread data. """
      self.thread_data_lock: threading.Lock = threading.Lock()
      """ Lock for per-thread data. """
      self.thread_stepping_count: int = 0
      """ The number of threads that are currently stepping. """
      self.runto_count: int = 0
      """ The number of threads that are currently running-to-cursor. """
      self.threadLocal: threading.local = threading.local()
      """ Thread-local data. """
      self.iid_to_value: Dict[int, object] = {}
      """ Map from internal id to value. """
      self.value_to_iid_data: Dict[int, ValueData] = {}
      """ Map from value id to value internal id data. """
      self.next_iid: int = 0
      """ Next free internal id. """
      self.iid_lock: threading.Lock = threading.Lock()
      """ Lock for internal id data. """
      self.type_to_type_iid: weakref.WeakKeyDictionary[type, int] = weakref.WeakKeyDictionary()
      """ Map from type to internal type id. """
      self.type_iid_to_type: weakref.WeakValueDictionary[int, type] = weakref.WeakValueDictionary()
      """ Map from internal type id to type. """
      self.next_type_iid: int = 0
      """ Next free internal type id. """
      self.type_iid_lock: threading.RLock = threading.RLock()
      """ Lock for internal type id data. """
      self.suspend_threads: Set[int] = set()
      """ Threads to be suspended by suspend command. """
      self.instruction_count: int = 0
      """ Number of global instructions waiting on INSTRUCTION callback. """
      self.exception_count: int = 0
      """ Number of global instructions waiting on EXCEPTION_HANDLED callback. """
      self.threadLocal.inDebug = 1
      """ True when in "debugger code", false when not. """
      # Register this debugger.
      mon.use_tool_id(self._tool_id, "JGrasp Python Debugger")
      # Register callbacks for system events.
      mon.register_callback(self._tool_id, E.LINE, self.handle_line)
      mon.register_callback(self._tool_id, E.INSTRUCTION, self.handle_instruction)
      mon.register_callback(self._tool_id, E.PY_START, self.handle_start)
      mon.register_callback(self._tool_id, E.PY_RESUME, self.handle_resume)
      mon.register_callback(self._tool_id, E.PY_RETURN, self.handle_return_or_yield)
      mon.register_callback(self._tool_id, E.PY_YIELD, self.handle_return_or_yield)
      mon.register_callback(self._tool_id, E.EXCEPTION_HANDLED, self.handle_catch)
   
      self.readThread: threading.Thread = threading.Thread(target=self.start_read, args=(sock,))
      self.readThread.start()
      """ The comms thread id. """
      
      # Enable PY_START event globally
      mon.set_events(self._tool_id, E.PY_START | E.PY_RESUME)


   def end(self) -> None:
      self.readThread.join()
      mon.register_callback(self._tool_id, E.LINE, None)
      mon.register_callback(self._tool_id, E.INSTRUCTION, None)
      mon.register_callback(self._tool_id, E.PY_START, None)
      mon.register_callback(self._tool_id, E.PY_RESUME, None)
      mon.register_callback(self._tool_id, E.PY_RETURN, None)
      mon.register_callback(self._tool_id, E.PY_YIELD, None)
      mon.register_callback(self._tool_id, E.EXCEPTION_HANDLED, None)


   def set_global_events(self) -> None:
      events: int = E.PY_START | E.PY_RESUME
      if self.instruction_count > 0:
         events += E.INSTRUCTION
      if self.exception_count > 0:
         events += E.EXCEPTION_HANDLED
      mon.set_events(self._tool_id, events)


   def _init_code_object(self, code: types.CodeType, code_id: int) -> None:
      """ Initializes data for a code object. This should be called within a file data lock.
   
   :param types.CodeType code: the code object.
   
   :param int code_id: the id of the code object. """
      
      # Note that in Python 3.9 and later, co_filename is always a full path unless it is a builtin.
      file_data: (FileData | None) = self.fn_to_file_data.get(code.co_filename)
      if file_data is None:
         fn = os.path.realpath(code.co_filename) if os.path.isabs(code.co_filename) else code.co_filename
        
         file_data = self.fn_to_file_data.get(fn)
         if file_data is None:
            #print(f"qualname {code.co_qualname} mod {inspect.getmodule(code)}")
            #*** need to pass in user exceptions
            file_data = FileData(fn)
            self.fn_to_file_data[fn] = file_data
            if fn is not code.co_filename:
               # Code path and its real path differ, store both for max lookup efficiency.
               self.fn_to_file_data[code.co_filename] = file_data
         else:
            self.fn_to_file_data[code.co_filename] = file_data
      self._coi_to_file_data[code_id] = file_data
      if not file_data.ignore:
         file_data.add_code(code, code_id, self)


   def handle_instruction(self, code: types.CodeType, ln: int) -> object:
      """ Handles an INSTRUCTION event. """
      
      if hasattr(self.threadLocal, "inDebug") and self.threadLocal.inDebug > 0:
         return mon.DISABLE
   
      try:
         self.threadLocal.inDebug = 1
      
         # Suspend.
         thread: int = threading.get_ident()
      
         with self.thread_data_lock:
            thread_data: (ThreadData | None) = self.thread_id_to_data.get(thread)
               
         rttd: ThreadData | None = None
         if self.runto_count > 0:
            with self.thread_data_lock:
               if thread_data is not None and thread_data.runto is not None:
                  # Run-to-cursor will be handled by handle_line().
                  rttd = thread_data
      
         stop: bool = False
         stop_description: str = ""
         with self.thread_data_lock:
            if thread in self.suspend_threads:
               stop = True
               stop_description = "user suspend"
               self._is_first_line_event = False
               self.instruction_count -= 1
               if self.instruction_count == 0:
                  self.set_global_events()
               if rttd is not None:
                  assert rttd.runto is not None
                  with self.file_data_lock:
                     runto_file_data: (FileData | None) = self.fn_to_file_data.get(rttd.runto.file)
                     if runto_file_data is not None:
                        runto_file_data.remove_runto(rttd.runto.line, self)
                  rttd.runto = None
                  self.runto_count -= 1
         if stop:
            # Note that any pending step will be cancelled inside _stop().
            self._stop(stop_description)
            return None
         elif rttd is not None:
            # If run-to-cursor is pending, ignore steps.
            if self.thread_stepping_count == 0:
               with self.file_data_lock:
                  if self._coi_to_file_data[id(code)].breakpoints.get(ln) is None:
                     # No breakpoint here and no multiple threads stepping.
                     return mon.DISABLE
            return None
      
         # Step.
         if self.thread_stepping_count == 0:
            return mon.DISABLE
         if not self.byte_code_steps:
            stop_at_out: bool = False
            with self.thread_data_lock:
               if thread_data is not None and thread_data.executing_step is not None:
                  if thread_data.executing_step.out:
                     # Landing after stepping out always stops at instruction rather than line.
                     stop_at_out = True
         
            if not stop_at_out:
               # Can't disable because line event for step could be at same location.
               return None
         code_id = id(code)
         with self.file_data_lock:
            file_data: FileData = self._coi_to_file_data[code_id]
            if file_data.ignore:
               if file_data.always_ignore:
                  return mon.DISABLE
               # Stop anyway if forced step.
               with self.thread_data_lock:
                  if thread_data is None or thread_data.executing_step is None or \
                        not thread_data.executing_step.forced:
                     return None
      
         # Check for stop at thread.
         with self.thread_data_lock:
            if thread_data is not None and thread_data.executing_step is not None:
               sd: StepData = thread_data.executing_step
               if sd.skip_first:
                  sd.skip_first = False
                  return None
               if sd.mode == StepMode.over:
                  if not JGraspPythonDebugger.stack_depth_greater(sd.depth + 1):
                     stop = True
                     stop_description = "step (over)"
               elif sd.mode == StepMode.out:
                  if not JGraspPythonDebugger.stack_depth_greater(sd.depth + (0 if sd.wait_for_out else 1)):
                     stop = True
                     stop_description = "step (out)"
               else:
                  stop = True
                  stop_description = "step (in)"
                  
               if not stop and not self.stop_at_breakpoints_while_stepping and self.thread_stepping_count == 1:
                  with self.file_data_lock:
                     if file_data.breakpoints.get(ln) is None:
                        # No breakpoint here, no multiple threads stepping, and not a step target.
                        return mon.DISABLE
         
         if not stop:
            return None
         
         self._is_first_line_event = False
         self._stop(stop_description)
         return None
      finally:
         self.threadLocal.inDebug = 0 


   def handle_line(self, code: types.CodeType, ln: int) -> object:
      """ Handles a LINE event. """
      
      if hasattr(self.threadLocal, "inDebug") and self.threadLocal.inDebug > 0:
         return mon.DISABLE
   
      try:
         self.threadLocal.inDebug = 1
      
         code_id: int = id(code)
      
         with self.file_data_lock:
            file_data: FileData = self._coi_to_file_data[code_id]
            if file_data.always_ignore:
               return mon.DISABLE
      
         stop: bool = False
         stop_description: str = ""
      
         with self.thread_data_lock:
            thread_data: (ThreadData | None) = self.thread_id_to_data.get(threading.get_ident())
      
         if self.runto_count > 0:
            with self.thread_data_lock:
               if thread_data is not None and thread_data.runto is not None:
                  if thread_data.runto.line == ln:
                     with self.file_data_lock:
                        if thread_data.runto.file == file_data.path:
                           stop = True
                           stop_description = "stop at cursor"
                           thread_data.runto = None
                           file_data.remove_runto(ln, self)
                           self.runto_count -= 1
                  if not stop:
                     # Ignore breakpoints in this thread while it is running to cursor.
                     if self.thread_stepping_count == 0:
                        with self.file_data_lock:
                           if file_data.breakpoints.get(ln) is None:
                              # No breakpoint here and no other thread stepping.
                              return mon.DISABLE
                     return None
         
         if not stop and file_data.ignore:
            # We are in an ignore-breakpoints code block, only forced step in should stop.
            if self.thread_stepping_count == 0 or self.byte_code_steps:
               # Not stepping or byte code steps (handled by handle_instruction()).
               return None
            # Stop anyway if forced step.
            with self.thread_data_lock:
               if thread_data is None or thread_data.executing_step is None or \
                     not thread_data.executing_step.forced:
                  return None
      
         step: bool = False
         will_stop: bool = False
         
         if not stop and self.thread_stepping_count > 0:
            # Check for stop at thread.
            with self.thread_data_lock:
               if thread_data is not None and thread_data.executing_step is not None:
                  sd: StepData = thread_data.executing_step
                  if sd.skip_first:
                     sd.skip_first = False
                     return None
                  step = True
                  if sd.mode == StepMode.over:
                     if not JGraspPythonDebugger.stack_depth_greater(sd.depth + 1):
                        if sd.byte:
                           will_stop = True
                        else:
                           stop = True
                           stop_description = "step (over)"
                  elif sd.mode == StepMode.out:
                     if not JGraspPythonDebugger.stack_depth_greater(sd.depth + (0 if sd.wait_for_out else 1)):
                        if sd.byte:
                           will_stop = True
                        else:
                           stop = True
                           stop_description = "step (out)"
                  else:
                     if sd.byte:
                        will_stop = True
                     else:
                        stop = True
                        stop_description = "step (in)"
               if not stop and not will_stop and not self.stop_at_breakpoints_while_stepping and\
                     self.thread_stepping_count == 1:
                  with self.file_data_lock:
                     if file_data.breakpoints.get(ln) is None:
                        # No breakpoint here, no multiple threads stepping, and not a step target.
                        return mon.DISABLE
         
         if (not stop and not will_stop and not step) or self.stop_at_breakpoints_while_stepping:
            # Check for stop at breakpoint.
            with self.file_data_lock:
               if file_data.breakpoints.get(ln) is not None:
                  stop = True
                  stop_description = "breakpoint"
         
            if not stop:
               if self.stop_at_entry and self._is_first_line_event and code_id == self.initial_code_object_id:
                  # Stop at main entry point.
                  stop = True
                  stop_description = "stop at entry point"
                  self.stop_at_entry = False
                  with self.code_ids_events_lock:
                     self.set_local_events(code, code_id)
         
            if not stop and self.thread_stepping_count == 0:
               return mon.DISABLE
         
         if stop:
            self._is_first_line_event = False
            self._stop(stop_description, True)
            return None
      finally:
         self.threadLocal.inDebug = 0


   def handle_resume(self, code: types.CodeType, instruction_offset: int) -> object:
      """ Handles a PY_RESUME event. """
       
      if hasattr(self.threadLocal, "inDebug") and self.threadLocal.inDebug > 0:
         return mon.DISABLE
   
      try:
         self.threadLocal.inDebug = 1
         if self.thread_stepping_count > 0:
            self.reset_step(1)
      finally:
         self.threadLocal.inDebug = 0


   def handle_start(self, code: types.CodeType, instruction_offset: int) -> object:
      """ Handles a PY_START event. """
       
      if hasattr(self.threadLocal, "inDebug") and self.threadLocal.inDebug > 0:
         return mon.DISABLE
   
      try:
         self.threadLocal.inDebug = 1
         thread_id: int = threading.get_ident()
      
         with self.thread_data_lock:
            # Check for new thread.
            thread_data = self.thread_id_to_data.get(thread_id)
            t: threading.Thread = threading.current_thread()
            if thread_data is None:
               thread_data = ThreadData(thread_id, t, self.isWorkbench)
               self.thread_id_to_data[thread_id] = thread_data
               self.write_event(f"new_thread#{thread_data.full_data(t)}\n")
            else:
               prev_state_data: str = thread_data.previous_state_data
               new_state_data: str = thread_data.state_data(t)
               if prev_state_data != new_state_data:
                  self.write_event(f"threads_change#{thread_id}|{new_state_data}\n")
      
         code_id = id(code)
         with self.file_data_lock:
            if code_id not in self._coi_to_file_data:
               self._init_code_object(code, code_id)
        
         if self.thread_stepping_count > 0:
            self.reset_step(1)
      finally:
         self.threadLocal.inDebug = 0


   def handle_return_or_yield(self, code: types.CodeType, instruction_offset: int, retval: object) -> object:
      """ Handles a PY_RETURN event. """
      if hasattr(self.threadLocal, "inDebug") and self.threadLocal.inDebug > 0:
         return mon.DISABLE
   
      try:
         self.threadLocal.inDebug = 1
         if self.thread_stepping_count > 0:
            self.reset_step(2)
         return None
      finally:
         self.threadLocal.inDebug = 0
      
      
   def handle_catch(self, code: types.CodeType, instruction_offset: int, exception: BaseException) -> object:
      """ Handles a PY_THROW event. """
      
      if hasattr(self.threadLocal, "inDebug") and self.threadLocal.inDebug > 0:
         return None
   
      try:
         self.threadLocal.inDebug = 1
         if self.thread_stepping_count > 0:
            self.reset_step(1)
         return None
      finally:
         self.threadLocal.inDebug = 0
      
      
   def reset_step(self, stack_delta: int) -> None:
      """ Resets the current step code block for a thread if the thread is stepping and the new location is a valid
   step target for the step, does nothing otherwise.
   
   :param int stack_delta: depth in the stack at which a step should stop. """
      
      thread_id: int = threading.get_ident()
      with self.thread_data_lock:
         thread_data = self.thread_id_to_data.get(thread_id)
         if thread_data is None:
            return
         sd: (StepData | None) = thread_data.executing_step
         if sd is None:
            return
         if sd.mode != StepMode.into and JGraspPythonDebugger.stack_depth_greater(sd.depth + stack_delta):
            return
         stack: List[FrameInfo] = inspect.stack(0)
         code_id: int = id(stack[stack_delta + 1].frame.f_code)
         with self.file_data_lock:
            file_data: FileData = self._coi_to_file_data[code_id]
            if file_data.ignore:
               if file_data.always_ignore:
                  return
               # Debugger will keep going if not forced step.
               if not sd.forced:
                  return
         
         # End step at current level.
         self.end_step(sd)
         sd.wait_for_out = False
         # Adjust step to new level.
         sd.set_code(len(stack) - stack_delta, stack, stack_delta == 2)
         self.setup_step(sd)


   def set_local_events(self, code: types.CodeType, code_id: int) -> None:
      """ Sets local events on a code object. code_ids_events_lock must be held when calling this.
   
   :param types.CodeType code: the code object.
   
   :param code_id: the code id. """
      # print(f"set events {code} val {self.code_ids_events[code_id].to_events(_stop_at_entry)} line {E.LINE} inst {E.INSTRUCTION} ret {E.PY_RETURN} yield {E.PY_YIELD}")
      stop_at_entry: bool = self.stop_at_entry and code_id == self.initial_code_object_id
      mon.set_local_events(self._tool_id, code, self.code_ids_events[code_id].to_events(stop_at_entry))


   def add_path(self, path: str) -> None:
      """ Adds a directory to the module search path if it is not already there.
   
   :param str path: the directory to be added. """
      if path not in sys.path:
         sys.path.insert(0, path)


   def add_breakpoint(self, file: str, line: int) -> None:
      """ Adds a breakpoint.
   
   :param str file: the name of the file containing the breakpoint.
   
   :param int line: the breakpoint line. """
      fn = os.path.realpath(file) if os.path.isabs(file) else file
      with self.file_data_lock:
         self.fn_to_file_data.setdefault(fn, FileData(fn)).add_breakpoint(line, self)


   def add_breakpoints(self, file: str, lines: List[int]) -> None:
      fn = os.path.realpath(file) if os.path.isabs(file) else file
      with self.file_data_lock:
         self.fn_to_file_data.setdefault(fn, FileData(fn)).add_breakpoints(lines, self)


   def remove_breakpoint(self, file: str, line: int) -> None:
      fn = os.path.realpath(file)
      with self.file_data_lock:
         file_data = self.fn_to_file_data.get(fn)
         if file_data is None:
            return
         file_data.remove_breakpoint(line, self)


   def process_command(self, command) -> None:
      parts: List[str] = command.removesuffix('\n').split("#")
      if not parts:
         # Throw exception
         return
      if parts[0] == 'c':
         response_id: int = int(parts[1])
         if parts[2] == "cont" or parts[2].startswith("step") or parts[2] == "stack" or parts[2] == "vars"\
               or parts[2] == "subvars" or parts[2] == "subvarslen" or parts[2] == "haselements" or parts[2] == "eval"\
               or parts[2] == "exec" or parts[2] == "exec_cont" or parts[2] == "str" or parts[2] == "run-to"\
               or parts[2] == "isinstance" or parts[2] == "issubclass" or parts[2] == "subvar":
            with self.thread_data_lock:
               thread_id: int = int(parts[3])
               thread_data = self.thread_id_to_data.get(thread_id)
               if thread_data is None:
                  self.write_event(f"response#{response_id}#n\n")
               elif not thread_data.stopped:
                  if thread_id in self.suspend_threads:
                     # Cancel pending suspend.
                     self.suspend_threads.discard(thread_id)
                     self.write_event(f"response#{response_id}#y\n")
                  else:
                     self.write_event(f"response#{response_id}#r\n")
               else:
                  if parts[2] == "stack":
                     thread_data.cmd_queue.put(f"stack#{response_id}")
                  elif parts[2] == "vars":
                     thread_data.cmd_queue.put(f"vars#{response_id}#{parts[4]}")
                  elif parts[2] == "str":
                     thread_data.cmd_queue.put(f"str#{response_id}#{parts[4]}#{parts[5]}#{parts[6]}")
                  elif parts[2] == "subvar":
                     thread_data.cmd_queue.put(f"subvar#{response_id}#{parts[4]}#{parts[5]}#{parts[6]}#{parts[7]}")
                  elif parts[2] == "subvars":
                     thread_data.cmd_queue.put(f"subvars#{response_id}#{parts[4]}#{parts[5]}#{parts[6]}#{parts[7]}#{parts[8]}")
                  elif parts[2] == "subvarslen":
                     thread_data.cmd_queue.put(f"subvarslen#{response_id}#{parts[4]}#{parts[5]}#{parts[6]}#{parts[7]}")
                  elif parts[2] == "haselements":
                     thread_data.cmd_queue.put(f"haselements#{response_id}#{parts[4]}#{parts[5]}")
                  elif parts[2] == "eval":
                     thread_data.cmd_queue.put(f"eval#{response_id}#{parts[4]}#{parts[5]}#{parts[6]}")
                  elif parts[2] == "exec":
                     thread_data.cmd_queue.put(f"exec#{response_id}#{parts[4]}#{parts[5]}")
                  elif parts[2] == "exec_cont":
                     thread_data.cmd_queue.put(f"exec_cont#{response_id}#{parts[4]}")
                  elif parts[2] == "isinstance":
                     thread_data.cmd_queue.put(f"isinstance#{response_id}#{parts[4]}#{parts[5]}#{parts[6]}")
                  elif parts[2] == "issubclass":
                     thread_data.cmd_queue.put(f"issubclass#{response_id}#{parts[4]}#{parts[5]}#{parts[6]}")
                  elif parts[2] == "run-to":
                     file = parts[4]
                     line = parts[5]
                     fn = os.path.realpath(file) if os.path.isabs(file) else file
                     with self.file_data_lock:
                        file_data = self.fn_to_file_data.setdefault(fn, FileData(fn))
                        ln: int = int(line)
                        file_data.add_runto(ln, self)
                        if thread_data.runto is not None:
                           old_file_data = self.fn_to_file_data.get(thread_data.runto.file)
                           if old_file_data is not None:
                              old_file_data.remove_runto(ln, self)
                        else:
                           self.runto_count += 1
                        thread_data.runto = RuntoData(fn, int(line))
                     thread_data.cmd_queue.put("cont")
                     self.write_event(f"response#{response_id}#y\n")
                  else: # Commands currently without a meaningful response.
                     if parts[2] == "cont" or parts[2] == "step":
                        # If waiting to suspend, cancel.
                        self.suspend_threads.discard(thread_id)
                     thread_data.cmd_queue.put(parts[2])
                     self.write_event(f"response#{response_id}#y\n")
         elif parts[2] == "suspend":
            with self.thread_data_lock:
               thread_id = int(parts[3])
               thread_data = self.thread_id_to_data.get(thread_id)
               if thread_data is None:
                  self.write_event(f"response#{response_id}#n\n")
               elif thread_data.stopped:
                  self.write_event(f"response#{response_id}#s\n")
               elif thread_id in self.suspend_threads:
                  self.write_event(f"response#{response_id}#w\n")
               else:
                  self.suspend_threads.add(thread_id)
                  self.instruction_count += 1
                  if self.instruction_count == 1:
                     self.set_global_events()
                  mon.restart_events()
                  self.write_event(f"response#{response_id}#y\n")
         elif parts[2] == "typename" or parts[2] == "mro":
            iid: int = int(parts[3])
            with self.type_iid_lock:
               if iid in self.type_iid_to_type:
                  try:
                     t: type = self.type_iid_to_type[iid]
                     if parts[2] == "typename":
                        self.write_event(f"response#{response_id}#y{self.full_type_name(t)}\n")
                     else:
                        tps: Tuple[type, ...] = inspect.getmro(t)
                        self.write_event(f"response#{response_id}#y{'|'.join([str(self.get_type_iid(cast(type, tp))) for tp in tps])}\n")
                        # print(f"type{t} res {'|'.join([str(self.get_type_iid(tp)) for tp in types])}")
                     return
                  except KeyError:
                     pass
               self.write_event(f"response#{response_id}#n\n")
         elif parts[2] == "info":
            pth: str = "\n   ".join([item for item in sys.path if item != os.path.dirname(__file__)])
            info: str = f"Name\n   {sys.implementation.name}\n\nVersion\n   {sys.version}\n\nPath\n   {pth}"
            self.write_event(f"response#{response_id}#y{self.encode(info)}\n")
      elif parts[0] == "path":
         file = parts[1]
         self.add_path(file)
      elif parts[0] == "break":
         file = parts[1]
         line = parts[2]
         self.add_breakpoint(file, int(line))
      elif parts[0] == "remove-break":
         file = parts[1]
         line = parts[2]
         self.remove_breakpoint(file, int(line))
      elif parts[0] == "breaks":
         file = parts[1]
         line = parts[2]
         self.add_breakpoints(file, [int(x) for x in line.split(',')])
      elif parts[0] == "sabws":
         self.stop_at_breakpoints_while_stepping = (parts[1] == "y")
      elif parts[0] == "byte-step":
         self.byte_code_steps = (parts[1] == "y")
      elif parts[0] == "check-threads":
         self.check_threads()
      elif parts[0] == "start":
         self.stop_at_entry = (parts[1] == "y")
         self.stop_at_end = self.stop_at_entry
         global init_event
         init_event.set()
      # elif parts[0] == "get_call_stack":
         # with self.thread_data_lock:
            # thread_id: int = int(parts[1])
            # thread_data = self.thread_id_to_data.get(thread_id)
            # if thread_data is not None and thread_data.stopped:
               # thread_data.cmd_queue.put("cont")
         #*** else return an error


   def pause_at_end(self) -> None:
      """ Halts at the end of debugging. Must be called with inDebug set to 1. """
      try:
         code: types.CodeType = compile("breakpoint()", "<hold at end>", "exec", 0, False, 0)
         self.write_event("hold\n")
         self.threadLocal.inDebug = 0
         exec(code, {"__name__": "__main__", "__file__": "<hold at end>", "__doc__": None, "__annotations__": {},
               "__loader__": os.__loader__})
         self.threadLocal.inDebug = 1
      except BaseException as e:
         pass
      finally:
         self.threadLocal.inDebug = 1


   def end_pending_steps(self) -> None:
      """ Ends pending steps if any. """
      with self.thread_data_lock:
         for thread_data in self.thread_id_to_data.values():
            sd: (StepData | None) = thread_data.executing_step
            if sd is not None:
               self.step_completed(sd, thread_data)


   def varMap(self, s: str) -> Dict[str, object]:
      """Decodes a variables argument.
   
   :param str s: the string to encode.
   
   :return: the variable to value map that was encoded.
   :rtype: Dict[str, int] """
      
      result: Dict[str, int] = {}
      if len(s) == 0:
         return result
      mappings: List[str] = s.split(",")
      with self.iid_lock:
         for mapping in mappings:
            name, val_iid_str = mapping.split("|")
            val: object = self.iid_to_value[int(val_iid_str)]
            result[name] = val
         
      return result


   def run(self, file: str) -> int:
      """ Runs the debugger
   
   :param str file: The source file to be debugged
   
   :return: The return value.
   :rtype: int """
      # Compile the target code.
      self.isWorkbench = file == "jgr_interactions"
      target : (types.CodeType | None)
      path: (Path | None) = None if self.isWorkbench else Path(file)
      if path:
         try:
            target = compile(path.read_text(), filename=file, mode='exec')
         except Exception as e:
            print("\n" + "".join(traceback.format_exception_only(e)), file=sys.stderr)
            return 0
      else:
         # Interactions shell.
         try:
            fn: str = "<interactions>"
            # The workbench/interactions thread is named and marked before execution.
            target = compile(
                  "import threading;threading.current_thread().name='jGRASP Workbench';del threading\npass",
                  filename=fn, mode='exec')
            self.add_breakpoint(fn, 2)
         except (SyntaxError, TypeError, ValueError) as e:
            print("\n" + "".join(traceback.format_exception_only(e)), file=sys.stderr)
            return 0
      # Arguments 1 and higher to debugger become 0 and higher to target.
      sys.argv.pop(0)
      # Comment the following line to debug the debugger with pdb when the breakpoint() function is called.
      sys.breakpointhook = self._bp_func_stop
      self.initial_code_object_id = id(target)
      ret_val: int = 0
      try:
         # Record the code object for this function, because if the user steps off the end of the program, line events
         # will still be enabled after exec(), and will be triggered when inDebug is set to 1.
         frame: (types.FrameType | None) = inspect.currentframe()
         if frame is None:
            print("\nUnable to debug: Python implementation does not have stack frame support.", file=sys.stderr)
            return 0
         code: types.CodeType = frame.f_code
         self._init_code_object(code, id(code))
         self.replace_stdin()
         try:
            self.threadLocal.inDebug = 0
            exec(target, {"__name__": "__main__", "__file__": file, "__doc__": None, "__annotations__": {},
                  "__loader__": os.__loader__})
            self.threadLocal.inDebug = 1
         except SystemExit as e:
            ret_val = e.code
         self.threadLocal.inDebug = 1
         
         if self.stop_at_end:
            # Need to turn autostep off
            self.end_pending_steps()
            self.pause_at_end()
      
         # Wait for any user-spawned threads to finish.
         main_thread: threading.Thread = threading.current_thread()
         while True:
            all_daemon: bool = True
            for thread in threading.enumerate():
               if not thread.daemon and thread != main_thread and thread != self.readThread:
                  all_daemon = False
                  break
            if all_daemon:
               break
            time.sleep(.5)
         # If the user has stepped off the end, events may still be enabled.
         self.instruction_count = 0
         mon.set_events(self._tool_id, 0)
      except BaseException as e:
         prn_lst = traceback.format_exception(e)
         if len(prn_lst) > 2 and debugger_file in prn_lst[1]:
            # Strip our base frame information.
            del prn_lst[1]
            files = [line for line in prn_lst if line.startswith("  File ")]
            if len(files) == 1:
               # Remove the "most recent call last" header since there is only one stack frame.
               del prn_lst[0]
         print("\n" + "".join(prn_lst), file=sys.stderr)
         ret_val = 1
      return ret_val
      
 
   def replace_stdin(self) -> None:
   
      class StdinWrapper:
      
         def __init__(self, dbg: JGraspPythonDebugger):
            self.dbg = dbg
      
         @property
         def line_buffering(self) -> bool:
            return sys.__stdin__.line_buffering
            
         @property
         def write_through(self) -> bool:
            return sys.__stdin__.write_through
      
         def reconfigure(self, **kwargs) -> None:
            sys.__stdin__.reconfigure(**kwargs)
      
         @property
         def encoding(self) -> str:
            return sys.__stdin__.encoding
            
         @property
         def errors(self) -> str | None:
            return sys.__stdin__.errors
            
         @property
         def newlines(self) -> str | Tuple[str, ...] | None:
            return sys.__stdin__.newlines
            
         @property
         def buffer(self):
            return sys.__stdin__.buffer
            
         def detach(self):
            return sys.__stdin__.detach()
      
         def read(self, size: int = -1, /) -> str:
            self.dbg.write_event("input_request\n")
            result: str = sys.__stdin__.read(size)
            self.dbg.write_event("input_done\n")
            return result
            
         def write(self, s: str, /) -> int:
            return sys.__stdin__.write(s)
          
         def __iter__(self) -> Iterator[str]:
            return self
            
         def __next__(self) -> str:
            self.dbg.write_event("input_request\n")
            result = sys.__stdin__.__next__()
            self.dbg.write_event("input_done\n")
            return result
            
         def close(self) -> None:
            sys.__stdin__.close()
            
         @property
         def closed(self) -> bool:
            return sys.__stdin__.closed
       
         def fileno(self) -> int:
            raise OSError(6, "jGRASP wrapper for stdin doesn't provide fileno.")
       
         def flush(self) -> None:
            sys.__stdin__.flush()
       
         def isatty(self) -> bool:
            return sys.__stdin__.isatty()
       
         def readable(self) -> bool:
            return sys.__stdin__.readable()
       
         def readline(self, size: int = -1, /) -> str:
            self.dbg.write_event("input_request\n")
            result:str = sys.__stdin__.readline(size)
            self.dbg.write_event("input_done\n")
            return result
      
         def readlines(self, hint: int = -1, /) -> List[str]:
            self.dbg.write_event("input_request\n")
            result: List[str] = sys.__stdin__.readlines(hint)
            self.dbg.write_event("input_done\n")
            return result
      
         def seek(self, offset: int, whence: int = os.SEEK_SET, /) -> int:
            return sys.__stdin__.seek(offset, whence)
      
         def seekable(self) -> bool:
            return sys.__stdin__.seekable()
          
         def tell(self) -> int:
            return sys.__stdin__.tell()
      
         def truncate(self, size: (int | None) = None, /) -> int:
            return sys.__stdin__.truncate(size)
       
         def writable(self) -> bool:
            return sys.__stdin__.writable()
         
         def writelines(self, lines: Iterable[str], /):
            sys.__stdin__.writelines(lines)
         
         def __del__(self) -> None:
            sys.__stdin__.__del__()
            
      sys.stdin = StdinWrapper(self)

 
   def _bp_func_stop(self) -> None:
      """ Breakpoint function handler. """
      try:
         self.threadLocal.inDebug = self.threadLocal.inDebug + 1 if hasattr(self.threadLocal, "inDebug") else 1
         if not self.stop_at_breakpoints_while_stepping and self.thread_stepping_count > 0:
            with self.thread_data_lock:
               thread_data: (ThreadData | None) = self.thread_id_to_data.get(threading.get_ident())
               if thread_data is not None and thread_data.executing_step is not None:
                  return
         self._stop("breakpoint function call", True)
      finally:
         self.threadLocal.inDebug = self.threadLocal.inDebug - 1
   
   
   def _stop(self, reason: str, from_bp: bool = False) -> None:
      """ Stops a thread.
   
   :param str reason: The reason for this stop.
   
   :param from_bp: True if this stop is from a breakpoint, false if it is not. """
   
      thread_id = threading.get_ident()
      with self.thread_data_lock:
         thread_data = self.thread_id_to_data.get(thread_id)
         if thread_data is None:
            thread_data = ThreadData(thread_id, threading.current_thread(), self.isWorkbench)
            self.thread_id_to_data[thread_id] = thread_data
         current_sd: (StepData | None) = thread_data.executing_step
         if current_sd is not None:
            # Step is completed or was interrupted with a user suspend.
            self.step_completed(current_sd, thread_data)
         if thread_data.stopped:
            return
         thread_data.stopped = True
         cmd_queue: queue.Queue[str] = thread_data.cmd_queue
      
      stack: List[FrameInfo] = inspect.stack(0)
      
      is_workbench: bool = len(stack) > 2 and stack[2].filename == "<interactions>"
      self.write_event(f"stopped#{thread_id}|{reason}|{'y' if is_workbench else 'n'}\n")
      
      while True:
         cmd: str = cmd_queue.get()
         parts: List[str] = cmd.split("#")
         if parts[0] == "cont":
            break
         elif parts[0].startswith("step"):
            # faulthandler.dump_traceback(all_threads=False)
            with self.thread_data_lock:
               sd: StepData = StepData(StepMode.into if "-in" in parts[0] else
                  StepMode.out if "-out" in parts[0] else StepMode.over, "forced" in parts[0],
                  len(stack) - 1, self.byte_code_steps, from_bp and self.byte_code_steps, stack)
               self.thread_stepping_count += 1
               self.setup_step(sd)
               thread_data.executing_step = sd
            break
         elif parts[0] == "stack":
            self.write_event(f"response#{parts[1]}#{self.get_stack_data(stack)}\n")
         elif parts[0] == "vars":
            self.write_event(f"response#{parts[1]}#{self.get_vars(stack, int(parts[2]))}\n")
         elif parts[0] == "subvar":
            self.write_event(f"response#{parts[1]}#{self.get_subvar(stack, int(parts[2]), int(parts[3]), parts[4], int(parts[5]))}\n")
         elif parts[0] == "subvars":
            self.write_event(f"response#{parts[1]}#{self.get_subvars(stack, int(parts[2]), int(parts[3]), int(parts[4]), int(parts[5]), int(parts[6]))}\n")
         elif parts[0] == "subvarslen":
            self.write_event(f"response#{parts[1]}#{self.get_subvars_len(stack, int(parts[2]), int(parts[3]), int(parts[4]), int(parts[5]))}\n")
         elif parts[0] == "haselements":
            self.write_event(f"response#{parts[1]}#{self.is_list(stack, int(parts[2]), int(parts[3]))}\n")
         elif parts[0] == "str":
            self.write_event(f"response#{parts[1]}#{self.get_str(stack, int(parts[2]), int(parts[3]), int(parts[4]))}\n")
         elif parts[0] == "eval":
            self.write_event(f"response#{parts[1]}#{self.eval(stack, int(parts[2]), JGraspPythonDebugger.decode(parts[3]), JGraspPythonDebugger.varMap(self, parts[4]))}\n")
         elif parts[0] == "exec":
            self.write_event(f"response#{parts[1]}#{self.exec(stack, int(parts[2]), JGraspPythonDebugger.decode(parts[3]))}\n")
         elif parts[0] == "exec_cont":
            self.write_event(f"response#{parts[1]}#{self.exec_cont(JGraspPythonDebugger.decode(parts[2]))}\n")
         elif parts[0] == "isinstance":
            self.write_event(f"response#{parts[1]}#{self.is_instance(stack, int(parts[2]), int(parts[3]), parts[4])}\n")
         elif parts[0] == "issubclass":
            self.write_event(f"response#{parts[1]}#{self.is_subclass(stack, int(parts[2]), int(parts[3]), parts[4])}\n")
   
      with self.thread_data_lock:
         thread_data.stopped = False
   
      flags: int = 0
      if thread_data.runto is not None:
         flags |= ThreadStateFlag.RUN_TO_CURSOR
      if thread_data.executing_step is not None:
         flags |= ThreadStateFlag.STEPPING
   
      self.write_event(f"resumed#{thread_id}|{flags}\n")
      
      
   def setup_step(self, sd: StepData) -> None:
      """ Sets event requests for a step.
   
   :param StepData sd: the step data. """
      with self.code_ids_events_lock:
         events: EventCounts = self.code_ids_events.setdefault(sd.code_id, EventCounts())
         events_changed: bool = False
         if not sd.wait_for_out:
            if sd.byte or sd.out:
               events_changed |= events.instruction == 0
               events.instruction += 1
            else:
               events_changed |= events.line == 0
               events.line += 1
         events_changed |= events.retrn == 0
         events.retrn += 1
         if events_changed:
            self.set_local_events(sd.code, sd.code_id)
         self.exception_count += 1
         if self.exception_count == 1:
            self.set_global_events()
         mon.restart_events()
   
   
   def end_step(self, sd: StepData) -> None:
      """ Sets event requests for a step that has completed or left the code block.
   
   :param StepData sd: the step data. """
      with self.code_ids_events_lock:
         events: EventCounts = self.code_ids_events.setdefault(sd.code_id, EventCounts())
         events_changed: bool = False
         if not sd.wait_for_out:
            if sd.byte or sd.out:
               events_changed |= events.instruction == 1
               events.instruction -= 1
            else:
               events_changed |= events.line == 1
               events.line -= 1
         events_changed |= events.retrn == 1
         events.retrn -= 1
         if events_changed:
            self.set_local_events(sd.code, sd.code_id)
         self.exception_count -= 1
         if self.exception_count == 1:
            self.set_global_events()
   
   
   def step_completed(self, sd: StepData, thread_data: ThreadData) -> None:
      """ Cleans up when a step is completed or becomes invalid (thread is paused during a step or thread ends).
   The thread_data_lock must be set when this is called.
   
   :param StepData sd: the step data.
   
   :param ThreadData thread_data: the thread data. """
      self.thread_stepping_count -= 1
      self.end_step(sd)
      thread_data.executing_step = None

   
   def get_stack_data(self, call_stack: List[FrameInfo]) -> str:
      """ Gets stack data.
   
   :param List[FrameInfo] call_stack: the call stack.
   
   :return: The stack data.
   :rtype: str """
      stack: List[str] = []
      first_user_frame: bool = False
      index: int = -1
      for fi in call_stack:
         index += 1
         if fi.filename == debugger_file:
            # Skip our top frames, our bottom frames, and the startup frames at the very bottom.
            if not first_user_frame:
               continue
            break
         first_user_frame = True
         mod_name: str = "<unknown>"
         mod_name_val: (Any | None) = fi.frame.f_globals.get('__name__')
         if isinstance(mod_name_val, str):
            mod_name = mod_name_val
         stack.append(f"{self.encode(fi.frame.f_code.co_qualname)}|{id(fi.frame.f_code)}|{self.encode(fi.filename)}|"
               f"{self.encode(mod_name)}|{fi.lineno}|{fi.frame.f_lasti}|{index}")
      
      return f"y{"%".join(stack)}"
 
 
   def get_vars(self, stack: List[FrameInfo], frame_index: int) -> str:
      """ Gets local variables.
   
   :param int frame_index: the stack frame index.
   
   :param List[FrameInfo] stack: the call stack.
   
   :return: The local variables data.
   :rtype: str """
      if frame_index >= len(stack):
         return f"nBad frame index {frame_index} with stack length {len(stack)}"
      result: List[str] = []
      fi: FrameInfo = stack[frame_index]
      co: types.CodeType = fi.frame.f_code
      result.append(f"{id(co)}")
      diff_locals_globals: bool = not fi.frame.f_locals is fi.frame.f_globals
      result.append(f"{"y" if diff_locals_globals else "n"}")
      arg_count: int = co.co_argcount
      pos_only_arg_count: int = co.co_posonlyargcount
      kw_only_arg_count: int = co.co_kwonlyargcount
      total_arg_count: int = arg_count + kw_only_arg_count
      ind: int = 0
      for n,v in fi.frame.f_locals.items():
         arg_type: str = "v" if ind >= total_arg_count else ("p" if ind < pos_only_arg_count else
               ("a" if ind < arg_count else "k"))
         tp: type = type(v)
         if arg_type == "v" and self.filtered(n, tp, v):
            continue
         st: str
         l: int
         st, l = self.get_disp_string_and_len(v, tp)
         t: int = self.get_type_iid(tp)
         encoded_n: str = self.encode(n)
         result.append(f"{encoded_n}|{encoded_n}|{arg_type}|{t}|{id(v)}|{self.get_iid(v)}|{l}|{self.encode(st)}|{self.has_subvars(v,0,self.ATTRIBUTES|self.ELEMENTS)}")
         ind += 1
      if diff_locals_globals:
         for n,v in fi.frame.f_globals.items():
            tp = type(v)
            if self.filtered(n, tp, v):
               continue
            st, l = self.get_disp_string_and_len(v, tp)
            # call get_subvars on v (extract internals)
            t = self.get_type_iid(tp)
            encoded_n = self.encode(n)
            result.append(f"{encoded_n}|{encoded_n}|g|{t}|{id(v)}|{self.get_iid(v)}|{l}|{self.encode(st)}|{self.has_subvars(v,0,self.ATTRIBUTES|self.ELEMENTS)}")
      return f"y{"%".join(result)}"
 

   def get_type_iid(self, t: type) -> int:
      """ Gets the internal id for a type.
   
   :param object t: the type.
   
   :return: an internal id for the type.
   :rtype: int """
      with self.type_iid_lock:
         if t in self.type_to_type_iid:
            try:
               return self.type_to_type_iid[t]
            except KeyError:  # Very unlikely, but mapping could have been removed after test.
               pass
         self.next_type_iid += 1
         self.type_to_type_iid[t] = self.next_type_iid
         self.type_iid_to_type[self.next_type_iid] = t
         return self.next_type_iid


   def get_iid(self, val: object) -> int:
      """ Gets the internal id for a value and increments its reference count.
   
   :param object val: the value.
   
   :return: an internal id for the value.
   :rtype: int """
      with self.iid_lock:
         val_id: int = id(val)
         if val_id in self.value_to_iid_data:
            data: ValueData = self.value_to_iid_data[val_id]
            data.ref_count += 1
            return data.iid
         self.next_iid += 1
         self.iid_to_value[self.next_iid] = val
         self.value_to_iid_data[val_id] = ValueData(self.next_iid, 1)
         return self.next_iid
      
      
   def free_iid(self, iid: int) -> None:
      """ Decreases the reference count of a value and frees it if the reference count reaches zero.
   
   :param int iid: the internal id of the value. """
      with self.iid_lock:
         val: object = self.iid_to_value[iid]
         val_id: int = id(val)
         data: ValueData = self.value_to_iid_data[val_id]
         if data.ref_count == 1:
            del self.iid_to_value[iid]
            del self.value_to_iid_data[val_id]
         else:
            data.ref_count = data.ref_count - 1
         
      
   def get_subvar(self, stack: List[FrameInfo], frame_index: int, val: int, name: str, flags: int) -> str:
      """ Gets a variable attribute.
   
   :param List[FrameInfo] stack: the call stack.
   
   :param int frame_index: the stack frame index.
   
   :param int val: the internal id for the value.
   
   :param str name: the attribute name.
   
   :param int flags: flags.
   
   :return: The response string containing variable attributes.
   :rtype: str """
   
      if frame_index >= len(stack):
         return f"nBad frame index {frame_index} with stack length {len(stack)}"
      result: List[str] = []
      fi: FrameInfo = stack[frame_index]
      co: types.CodeType = fi.frame.f_code
      result.append(f"{id(co)}")
      
      r: object = self.iid_to_value[val]
   
      v: Any = getattr(r, name)
      tp: type = type(v)
      st: str
      l: int
      st, l = self.get_disp_string_and_len(v, tp)
      t: int = self.get_type_iid(tp)
      result.append(f"{self.encode(name)}|{self.encode(f".{name}")}|a|{t}|{id(v)}|{self.get_iid(v)}|{l}|"
            f"{self.encode(st)}|{self.has_subvars(v,0,self.ATTRIBUTES|self.ELEMENTS)}")
   
      return f"y{"%".join(result)}"


   def get_subvars(self, stack: List[FrameInfo], frame_index: int, val: int, start: int, length: int, flags: int) -> str:
      """ Gets variable attributes.
   
   :param List[FrameInfo] stack: the call stack.
   
   :param int frame_index: the stack frame index.
   
   :param int val: the internal id for the value.
   
   :param int start: the start index for results.
   
   :param int length: the maximum number of attributes, or -1 for all attributes beginning with start.
   
   :param int flags: flags.
   
   :return: The response string containing variable attributes.
   :rtype: str """
   
      if frame_index >= len(stack):
         return f"nBad frame index {frame_index} with stack length {len(stack)}"
      result: List[str] = []
      fi: FrameInfo = stack[frame_index]
      co: types.CodeType = fi.frame.f_code
      result.append(f"{id(co)}")
      
      r: object = self.iid_to_value[val]
   
      members: List[Tuple[str, str, Any]] = cast(List[Tuple[str, str, Any]],
            self.get_members(r, None, start, start + length if length > 0 else -1, flags))
      for n, fn, v in members:
         tp: type = type(v)
         st: str
         l: int
         st, l = self.get_disp_string_and_len(v, tp)
         t: int = self.get_type_iid(tp)
         result.append(f"{self.encode(n)}|{self.encode(fn)}|a|{t}|{id(v)}|{self.get_iid(v)}|{l}|{self.encode(st)}|"
               f"{self.has_subvars(v,0,self.ATTRIBUTES|self.ELEMENTS)}")
   
      return f"y{"%".join(result)}"


   def get_subvars_len(self, stack: List[FrameInfo], frame_index: int, val: int, max_check: int, flags: int) -> str:
      """ Gets the length of variable attributes.
   
   :param List[FrameInfo] stack: the call stack.
   
   :param int frame_index: the stack frame index.
   
   :param int val: the internal id for the value.
   
   :param int flags: flags.
   
   :param int max_check: maximum length to check, plus one.
   
   :return: The response string containing the length. If this is equal to max then all have not been included.
   :rtype: str """
   
      if frame_index >= len(stack):
         return f"nBad frame index {frame_index} with stack length {len(stack)}"
      r: object = self.iid_to_value[val]
   
      result: int = cast(int, self.get_members(r, "len", 0, max_check, flags)) * (2 if isinstance(r, dict) else 1)
      return f"y{result}"


   def has_subvars(self, val: object, start: int, flags: int) -> str:
      """ Determines if a value has more than a specified number of attributes.
   
   :param object val: the internal id for the value.
   
   :param int start: the start index for testing.
   
   :param int flags: flags
   
   :return: True if the variable has more than 'start' attributes, False if it does not.
   :rtype: bool """
      return "t" if self.get_members(val, "has", start, start + 1, flags) else "f"


   def is_list(self, stack: List[FrameInfo], frame_index: int, val: int) -> str:
      """ Determines if a value is a list type.
   
   :param List[FrameInfo] stack: the call stack.
   
   :param int frame_index: the stack frame index.
   
   :param int val: the internal id for the value.
   
   :return: The response string.
   :rtype: str """
      if frame_index >= len(stack):
         return f"nBad frame index {frame_index} with stack length {len(stack)}"
      r: object = self.iid_to_value[val]
      return "yt" if self.get_members(r, "exists", 0, 1, self.ELEMENTS) else "yf"


   def get_str(self, stack: List[FrameInfo], frame_index: int, val: int, max_len: int) -> str:
      """ Gets the str() result for a value.
   
   :param List[FrameInfo] stack: the call stack.
   
   :param int frame_index: the stack frame index.
   
   :param int val: the internal id for the value.
   
   :param int max_len: the maximum length of the result, or zero for unlimited length.
   
   :return: The response string.
   :rtype: str """
   
      if frame_index >= len(stack):
         return f"nBad frame index {frame_index} with stack length {len(stack)}"
      r: object = self.iid_to_value[val]
      #*** Add clever stuff to reduce string computations for common types?
      return f"y{self.encode(str(r) if max_len <= 0 else str(r)[0:max_len])}"


   def eval(self, stack: List[FrameInfo], frame_index: int, expr_in: str, inject_vars: Dict[str, object]) -> str:
      """ Gets evaluated expression attributes.
   
   :param List[FrameInfo] stack: the call stack.
   
   :param int frame_index: the stack frame index.
   
   :param str expr_in: the full variable name.
   
   :param inject_vars: map from variable names to be injected to their values.
   
   :return: The response string.
   :rtype: str """
   
      if frame_index >= len(stack):
         return f"nBad frame index {frame_index} with stack length {len(stack)}."
      result: List[str] = []
      fi: FrameInfo = stack[frame_index]
      co: types.CodeType = fi.frame.f_code
      result.append(f"{id(co)}")
      
      def iter_value_by_index(val: Iterable[Never], ind: int) -> object:
         element_ind: int
         element: object
         for element_ind, element in enumerate(val):
            if element_ind == ind:
               return element
         raise IndexError(f"Iterator index out of range.")
      
      def dict_value_by_index(val: dict, ind: int) -> object:
         for element_ind, element in enumerate(val.items()):
            if element_ind == ind:
               return element[1]
         raise IndexError(f"Dictionary value iterator index out of range.")
      
      expr: str = expr_in
   
      iter_index: str = r"«(val |key )?(\d+)»"
      if re.search(iter_index, expr) is not None:
         parts: List[str] = re.split(iter_index, expr)
         expr_parts: Deque[str] = deque()
         for i in range(0, len(parts), 3):
            if i + 2 >= len(parts):
               expr_parts.append(parts[i])
            else:
               if parts[i + 1] is not None and parts[i + 1].startswith("v"):
                  expr_parts.appendleft("__JGRASP__829J_6h_dvbi__(")
               else:
                  expr_parts.appendleft("__JGRASP__829J_6h_ivbi__(")
               expr_parts.append(parts[i])
               expr_parts.append(",")
               expr_parts.append(parts[i + 2])
               expr_parts.append(")")
         expr = "".join(expr_parts)
   
      r: object
      try:
         r = eval(expr, fi.frame.f_globals, fi.frame.f_locals | { "__JGRASP__829J_6h_ivbi__" : iter_value_by_index,
               "__JGRASP__829J_6h_dvbi__" : dict_value_by_index} | inject_vars)
      except SystemExit as e:
         r = e.code
      except Exception as e:
         return self.encode(f"n({self.encode(str(e))})")
   
      tp: type = type(r)
      st: str
      l: int
      st, l = self.get_disp_string_and_len(r, tp)
      t: int = self.get_type_iid(tp)
      result.append(f"|{self.encode(expr)}|v|{t}|{id(r)}|{self.get_iid(r)}|{l}|{self.encode(st)}|{self.has_subvars(r,0,self.ATTRIBUTES|self.ELEMENTS)}")
   
      return "y" + "%".join(result)


   def exec_cont(self, seg: str) -> str:
      """ Determines if a code segment is possibly the start of valid code.
   
   :param str seg: the code segment.
   
   :return: the exec_cont response.
   :rtype: str """
   
      try:
         tree: ast.Module = ast.parse(seg, "<interactions>", type_comments = True)
      except ValueError as e:
         return "yn"
      except SyntaxError as e:
         if JGraspPythonDebugger.is_continue_error(e, seg):
            return "yy"
      return "yn"
         
    
   @staticmethod
   def name_to_type(fi: FrameInfo, type_name: str) -> type | None:
      try:
         module_name: (str | None) = None
         if type_name.startswith('('):
            parts: List[str] = type_name[1:].split(')')
            if len(parts) != 2 or not parts[1] or parts[1][0] != '.':
               return None
            module_name = parts[0]
            type_name = parts[1][1:]
      
         tp: Any
         sub_types: List[str] = type_name.split(".")
         try:
            if module_name == None or module_name == "__main__":
               tp = fi.frame.f_globals[sub_types[0]]
               sub_types = sub_types[1:]
            else:
               tp = sys.modules[module_name]
            for name in sub_types:
               tp = getattr(tp, name)
         except AttributeError as e:
            return None
         if not isinstance(tp, type):
            return None
         return tp
      except (KeyError, NameError) as e:
         return None
      
      
   def is_instance(self, stack: List[FrameInfo], frame_index: int, val_iid: int, type_name: str) -> str:
      if frame_index >= len(stack):
         return f"nBad frame index {frame_index} with stack length {len(stack)}"
      fi: FrameInfo = stack[frame_index]
      tp: (type | None) = JGraspPythonDebugger.name_to_type(fi, type_name)
      if tp is None:
         # Bad type
         return "yf"
      try:
         v: object = self.iid_to_value[val_iid]
         if isinstance(v, tp):
            return "yt"
      except (KeyError, NameError) as e:
         pass
   
      return "yf"
         
         
   def is_subclass(self, stack: List[FrameInfo], frame_index: int, type_iid: int, type_name: str) -> str:
      if frame_index >= len(stack):
         return f"nBad frame index {frame_index} with stack length {len(stack)}"
      fi: FrameInfo = stack[frame_index]
      tp: (type | None) = JGraspPythonDebugger.name_to_type(fi, type_name)
      if tp is None:
         # Bad type
         return "yf"
      try:
         t: type = self.type_iid_to_type[type_iid]
         if issubclass(t, tp):
            return "yt"
      except (KeyError, NameError) as e:
         pass
   
      return "yf"
  
  
   def exec(self, stack: List[FrameInfo], frame_index: int, seg: str) -> str:
      """ Gets evaluated code attributes.
   
   :param List[FrameInfo] stack: the call stack.
   
   :param int frame_index: the stack frame index.
   
   :param str seg: the code segment.
   
   :return: the exec response.
   :rtype: str """
   
      if frame_index >= len(stack):
         return f"nBad frame index {frame_index} with stack length {len(stack)}."
      fi: FrameInfo = stack[frame_index]
      co: types.CodeType = fi.frame.f_code
      
      try:
         tree: ast.Module = ast.parse(seg, "<interactions>", type_comments = True)
      except ValueError as e:
         return f"n({self.encode(str(e))})"
      except SyntaxError as e:
         return JGraspPythonDebugger.syntax_error_message(e)
      
      stmts: List[ast.stmt] = tree.body
      expr_code: (types.CodeType | None) = None
      if len(stmts) > 0 and isinstance(stmts[-1], ast.Expr):
         # Pull off expression from final expression statement.
         expr = cast(ast.Expr, stmts.pop()).value
         try:
            expr_code = compile(ast.Expression(expr), "<interactions>", "eval", 0, False, 0)
         except ValueError as e:
            return f"n({self.encode(str(e))})"
         except SyntaxError as e:
            return JGraspPythonDebugger.syntax_error_message(e)
      
      if len(stmts) > 0:
         try:
            code: types.CodeType = compile(tree, "<exec>", "exec", 0, False, 0)
         except ValueError as e:
            return f"n({self.encode(str(e))})"
         except SyntaxError as e:
            return JGraspPythonDebugger.syntax_error_message(e)
            
         try:
            exec(code, fi.frame.f_globals, fi.frame.f_locals)
         except SystemExit as e:
            return self.encode(f"yn<exit with argument {e.code}>")
         except ValueError as e:
            return f"n({self.encode(str(e))})"
         except SyntaxError as e:
            return JGraspPythonDebugger.syntax_error_message(e)
         except NameError as e:
            return f"m{self.encode(e.name)}|({self.encode(str(e))})"
         except Exception as e:
            return f"n({self.encode(str(e))})"
      
      if not expr_code:
         # Empty result (no expression at end).
         return "yy"
   
      r: object
      try:
         r = eval(expr_code, fi.frame.f_globals, fi.frame.f_locals)
      except SystemExit as e:
         return self.encode(f"yn<exit with argument {e.code}>")
      except ValueError as e:
         return f"n({self.encode(str(e))})"
      except SyntaxError as e:
         return JGraspPythonDebugger.syntax_error_message(e)
      except NameError as e:
         return f"m{self.encode(e.name)}|({self.encode(str(e))})"
      except Exception as e:
         return f"n({self.encode(str(e))})"
   
      return f"yn{self.encode(str(r))}"


   @staticmethod
   def syntax_error_message(e: SyntaxError) -> str:
      """Creates a detailed response message for an interactions syntax error.
   
   :param e the syntax error.  
   
   :return: the encoded detailed message.
   :rtype: str """
      details: Tuple = e.args[1]
      msg: str = f"ninteractions:{details[1]}:{details[2]}:{details[4]}:{details[5]}: {e}"
      if details[3]:
         msg += f"\n{details[3]}"
         if not (details[3].endswith("\n") or details[3].endswith("\r")):
            msg += "\n"
         msg += ' ' * details[2]
         msg += "^\n"
      return JGraspPythonDebugger.encode(msg)


   @staticmethod
   def is_continue_error(e: SyntaxError, txt: str) -> bool:
      """Determines if a syntax error indicates the end of valid code.
   
   :param e the syntax error.
   
   :param txt the source code text. 
   
   :return: true if the error indicates that the text is the beginning of valid code, false if it does not.
   :rtype: str """
      message: str = str(e)
      if message.startswith("unterminated triple-quoted string literal")\
            or message.startswith("'(' was never closed")\
            or message.startswith("'[' was never closed")\
            or message.startswith("'{' was never closed"):
         return True
      if not message.startswith("expected an indented block"):
         return False
      details: Tuple = e.args[1]
      line: int = details[1]
      col: int = details[2]
      lines: List[str] = re.split(r'\n|\r\n|\r', txt)
      if len(lines) > 0 and lines[-1] == "":
         lines.pop()
      return len(lines) == line and len(lines[line - 1]) == col - 1


   def get_members(self, val: object, test_type: (str | None), start: int, end: int, flags: int) -> (List[Tuple[str, str, Any]] | bool | int):
   
      exists: bool = test_type == "exists" # Should only be used with ELEMENTS.
      test: bool = test_type == "has"
      get_len: bool = test_type == "len"
   
      result: List[Tuple[str, str, Any]] = []
      
      total_ind: int = 0
      if flags & self.ATTRIBUTES:
         n: str
         v: object
         for n, v in inspect.getmembers_static(val):
            if inspect.ismemberdescriptor(v):
               # Handle __slots__
               try:
                  v = getattr(val, n)
               except AttributeError:
                  continue
            if self.filtered(n, type(v), v):
               continue
            if test:
               return True
            if total_ind < start:
               continue
            if 0 <= end <= total_ind:
               break
            total_ind += 1
            if not get_len:
               result.append((n, f".{n}", v))
      #result: List[Tuple[str, object]] = inspect.getmembers_static(v)
      rem_start: int = start - total_ind
      rem_end: int = end - total_ind if end >= 0 else -1
   
      if flags & self.ELEMENTS != 0 and (end < 0 or total_ind < end):
         if isinstance(val, str) or isinstance(val, list) or isinstance(val, tuple):
            if exists:
               return True
            for i, element in enumerate(val[rem_start:rem_end] if rem_end >= 0 else val[rem_start:]):
               if test:
                  return True
               total_ind += 1
               n = f"[{i}]"
               if not get_len:
                  result.append((n, n, element))
         elif isinstance(val, dict):
            if exists:
               return True
            for i, element in islice(enumerate(val.items()), rem_start, rem_end if rem_end >= 0 else None):
               if test:
                  return True
               total_ind += 1
               if not get_len:
                  k: str = f"«key {i}»"
                  result.append((k, k, element[0]))
                  kv: str = f"«val {i}»"
                  result.append((kv, kv, element[1]))
         elif hasattr(val, "__len__") and (hasattr(val, "__iter__") or hasattr(val, "__getitem__")) \
               and not isinstance(val, type):
            # Note that the "type" type has __len__ and __iter__ methods but is not iterable.
            try:
               it = iter(cast(Iterable, val))
            except TypeError:
               # Not iterable.
               return False if exists or test else result
            if exists:
               return True
            for i, element in islice(enumerate(it), start, end if end >= 0 else None):
               if test:
                  return True
               if total_ind < start:
                  continue
               if 0 <= end <= total_ind:
                  break
               total_ind += 1
               n = f"«{i}»"
               if not get_len:
                  result.append((n, n, element))
      return False if exists or test else (total_ind if get_len else result)


   def filtered(self, name: str, tp: type, v: Any) -> bool:
      # return False
      if name.startswith("__") and name.endswith("__"):
         return True
      if tp in JGraspPythonDebugger.filterTypes:
         return True
      if isinstance(v, type):
         return True
      if isinstance(v, typing._SpecialGenericAlias):
         return True
      return False

 
   def full_type_name(self, t: type) -> str:
      """ Gets the full name for a type.
   
   :param type t: the type.
   
   :return: the full type name.
   :rtype: str """
      result: List[str] = []
      try:
         m: str = t.__module__
         if m is not None:
            result.append(f"({m})")
      except Exception:
         pass
      try:
         result.append(t.__qualname__)
      except Exception:
         result.append(t.__name__)
      return ".".join(result)
         
 
 
   def get_disp_string_and_len(self, value: Any, tp: type) -> Tuple[str, int]:
      l: int = -1
      if value is None:
         return "None", l
      if hasattr(value, "__len__"):
         try:
            l = value.__len__()
         except Exception:
            # Debug ended.
            pass
      if issubclass(tp, int) or issubclass(tp, float) or issubclass(tp, complex) or issubclass(tp, bool)\
            or issubclass(tp, type):
         return str(value), l
      if issubclass(tp, str):
         ds: str = value[0:15]
         ds = ds.replace("\\", "\\\\").replace("\\r", "\\\\r").replace("\\n", "\\\\n").replace("\\b", "\\\\b")\
               .replace("\\f", "\\\\f").replace("\\\"", "\\\\\"")
         if len(ds) < 15:
            return f"\"{ds}\"", l
         return f"\"{ds[0:12]}\"...", l
      if l >= 0:
         if hasattr(value, "__iter__"):
            strings: List[str] = []
            total_len: int = 1
            i: int = 0
            for element in value:
               el_str, el_len = self.get_disp_string_and_len(element, type(element))
               el_str_len: int = len(el_str)
               if total_len + el_str_len + i + 1 >= 15:
                  break
               strings.append(el_str)
               total_len = total_len + el_str_len
               if total_len + i + 1 >= 15:
                  break
               i += 1
            st: str = ",".join(strings)
            if issubclass(tp, str) or issubclass(tp, list) or issubclass(tp, tuple):
               if i >= l:
                  return f"[{st}]", l
               return f"[{st}", l
            if i >= l:
               return f"<{st}>", l
            return f"<{st}", l
   
      return "", l


   def check_threads(self) -> None:
      """ Checks for dead or changed threads. """
      id_to_thread: Dict[int, threading.Thread] = {}
      for t in threading.enumerate():
         if t.ident is not None:
            id_to_thread[t.ident] = t
      with self.thread_data_lock:
         removed = [key for key in self.thread_id_to_data.keys() if key not in id_to_thread]
         for thread_id in removed:
            self.end_thread(thread_id)
            #del self.thread_id_to_data[id]
            #self.suspend_threads.discard(id)
         changed: List[str] = []
         for thread_id, t in id_to_thread.items():
            thread_data: (ThreadData | None) = self.thread_id_to_data.get(thread_id)
            if thread_data is None:
               continue
            prev_state_data: str = thread_data.previous_state_data
            new_state_data: str = thread_data.state_data(t)
            if prev_state_data != new_state_data:
               changed.append(f"{thread_data.thread_id}|{new_state_data}")
         if removed:
            self.write_event(f"threads_died#{"|".join(str(tid) for tid in removed)}\n")
         if changed:
            self.write_event(f"threads_change#{"|".join(changed)}\n")


   def end_thread(self, thread_id: int) -> None:
      thread_data: (ThreadData | None) = self.thread_id_to_data.pop(thread_id, None)
      self.suspend_threads.discard(thread_id)
      if thread_data is None:
         return
      sd: (StepData | None) = thread_data.executing_step
      if sd is None:
         return
      # Clean up uncompleted step.
      self.step_completed(sd, thread_data)


   def start_read(self, sock: socket.socket) -> None:
      self.threadLocal.inDebug = 1
   
      #*** Note that this is the message size limit.
      buffer_size: int = 4096
      buffer : memoryview = memoryview(bytearray(buffer_size))
      index: int = 0
      pos: int = 0
      while True:
         try:
            num_read: int = sock.recv_into(buffer[index:], min(10, len(buffer[index:])))
            if num_read == 0:
               # print("Connection closed.")
               break
            index += num_read
            if index == buffer_size:
               # Wrap around.
               buffer[0:index - pos] = buffer[pos:index]
               index = buffer_size - pos
               pos = 0
            new_pos: int = index - num_read
            while pos < index:# and (nl_pos := find_nl(buffer[pos:index])) >= 0:
               while new_pos < index and buffer[new_pos] != ord('\n'):
                  new_pos += 1
               if new_pos < index: # Found newline.
                  message = buffer[pos : new_pos + 1].tobytes().decode()
                  self.process_command(message)
                  new_pos += 1
                  pos = new_pos
               else:
                  break
            if pos == index:
               # Common case where input ends with newline. Reset to avoid wrapping.
               index = 0
               pos = 0
         except ConnectionResetError:
            print("Connection reset.")
            break
         except ConnectionAbortedError:
            # Debug ended.
            break
         #*** This happens on Mac. May need some better way to end this thread.
         except OSError:
            # Debug ended.
            break
  
  
   def write_event(self, line: str) -> None:
      """ Writes an event.
   
   :param str line: the event text. """
      #*** Change to send() on separate thread later?
      self._sock.sendall(line.encode('utf-8'))


   @staticmethod
   def stack_depth_greater(size: int) -> bool:
      """Determines if the stack size for caller's frame is greater than some value.
   
   :param size the size to test.  
   
   :return: true if the stack depth of the caller is greater than size, false if it is not.
   :rtype: bool """
      try:
         sys._getframe(size)
         return True
      except ValueError:
         return False


   @staticmethod
   def encode(s: str) -> str:
      """Encodes a response.
   
   :param str s: the string to encode.
   
   :return: the encoded string.
   :rtype: str """
      return s.translate(JGraspPythonDebugger.encodeTable)

   
   decodeDict: Dict[str, str] = {"\\": "\\", "n": "\n", "r": "\r", "h": "#", "p": "%", "b": "|"}
   """ Command element decoding table. """


   @staticmethod
   def decode(s: str) -> str:
      """Decodes a command argument.
   
   :param str s: the string to decode.
   
   :return: the decoded string.
   :rtype: str """
      if "\\" not in s:
         return s
      result: List[str] = []
      escaped: bool = False
      for c in s:
         if escaped:
            if c in JGraspPythonDebugger.decodeDict:
               result.append(JGraspPythonDebugger.decodeDict[c])
            else:
               result.append(c)
            escaped = False
         else:
            if c == '\\':
               escaped = True
            else:
               result.append(c)
      return "".join(result)


# def get_type(full_type_name: str) -> (type | None):
### import and unimport needed modules?
   # parts: List[str] = full_type_name.split(".")
   # module_name: (str | None) = parts[0] if len(parts) > 1 else None
   # if module_name == "builtins":
      # try: name__
         # return getattr(builtins, type_name) 
      # except AttributeError:
         # return None
   # try: 
      # obj = globals()[type_name] 
   # except KeyError: 
      # return None 
   # return repr(obj) if isinstance(obj, type) else None
     
     
def find_nl(b : memoryview) -> int:
   i: int = 0
   while i < len(b):
      if b[i] == ord('\n'):
         return i
      i += 1
   return -1 
   
   
if __name__ == '__main__':
   # faulthandler.enable()
   # faulthandler.dump_traceback_later(30)
   arg_parser: argparse.ArgumentParser = argparse.ArgumentParser()
   arg_parser.add_argument("file", help="file to debug")
   arg_parser.add_argument("args", nargs="*", help="arguments to pass to file")
   args = arg_parser.parse_args()

   cmd_sock: socket.socket = socket.socket()
   cmd_sock.bind(("localhost", 0))
   addr = cmd_sock.getsockname()[1]
   print(f"Listening at address:{addr}")
   cmd_sock.listen()
   _sock, addr = cmd_sock.accept()
   
   init_event = threading.Event()
   
    # Debug object must exist before I/O starts.
   debugger: JGraspPythonDebugger = JGraspPythonDebugger(_sock, args.file == "jgr_interactions")

   # Wait for initial breakpoint blast.
   ret_val: int = 0
   if not init_event.wait(30.0):
      print("No initialization after 30 seconds. Exiting debugger.")
   else:
      debugger.threadLocal.inDebug = 1
      ret_val = debugger.run(args.file)
   
   debugger.write_event(f"exit\n")
   debugger.end()
   sys.stdin = sys.__stdin__
   _sock.close()
   sys.exit(ret_val)
