Skip to content
46 changes: 29 additions & 17 deletions python-ecosys/debugpy/dap_monitor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python3

Check failure on line 1 in python-ecosys/debugpy/dap_monitor.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (EXE001)

python-ecosys/debugpy/dap_monitor.py:1:1: EXE001 Shebang is present but file is not executable
"""DAP protocol monitor - sits between VS Code and MicroPython debugpy."""

import socket
Expand All @@ -6,6 +6,7 @@
import json
import time
import sys
import argparse

class DAPMonitor:
def __init__(self, listen_port=5679, target_host='127.0.0.1', target_port=5678):
Expand All @@ -15,35 +16,35 @@
self.target_port = target_port
self.client_sock = None
self.server_sock = None

def start(self):
"""Start the DAP monitor proxy."""
print(f"DAP Monitor starting on port {self.listen_port}")
print(f"Will forward to {self.target_host}:{self.target_port}")
print("Start MicroPython debugpy server first, then connect VS Code to port 5679")

# Create listening socket
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(('127.0.0.1', self.listen_port))
listener.listen(1)

print(f"Listening for VS Code connection on port {self.listen_port}...")

try:
# Wait for VS Code to connect
self.client_sock, client_addr = listener.accept()
print(f"VS Code connected from {client_addr}")

# Connect to MicroPython debugpy server
self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_sock.connect((self.target_host, self.target_port))
print(f"Connected to MicroPython debugpy at {self.target_host}:{self.target_port}")

# Start forwarding threads
threading.Thread(target=self.forward_client_to_server, daemon=True).start()
threading.Thread(target=self.forward_server_to_client, daemon=True).start()

print("DAP Monitor active - press Ctrl+C to stop")
while not self.disconnect:
time.sleep(1)
Expand All @@ -54,7 +55,7 @@
print(f"Error: {e}")
finally:
self.cleanup()

def forward_client_to_server(self):
"""Forward messages from VS Code client to MicroPython server."""
try:
Expand All @@ -65,7 +66,7 @@
self.send_raw_data(self.server_sock, data)
except Exception as e:
print(f"Client->Server forwarding error: {e}")

def forward_server_to_client(self):
"""Forward messages from MicroPython server to VS Code client."""
try:
Expand All @@ -76,7 +77,7 @@
self.send_raw_data(self.client_sock, data)
except Exception as e:
print(f"Server->Client forwarding error: {e}")

def receive_dap_message(self, sock, source):
"""Receive and log a DAP message."""
try:
Expand All @@ -87,26 +88,26 @@
if not byte:
return None
header += byte

# Parse content length
header_str = header.decode('utf-8')
content_length = 0
for line in header_str.split('\r\n'):
if line.startswith('Content-Length:'):
content_length = int(line.split(':', 1)[1].strip())
break

if content_length == 0:
return None

# Read content
content = b""
while len(content) < content_length:
chunk = sock.recv(content_length - len(content))
if not chunk:
return None
content += chunk

# Parse and Log the message
message = self.parse_dap(source, content)
self.log_dap_message(source, message)
Expand Down Expand Up @@ -162,7 +163,7 @@
sock.send(data)
except Exception as e:
print(f"Error sending data: {e}")

def cleanup(self):
"""Clean up sockets."""
if self.client_sock:
Expand All @@ -171,5 +172,16 @@
self.server_sock.close()

if __name__ == "__main__":
monitor = DAPMonitor()
monitor.start()

parser = argparse.ArgumentParser(description="DAP protocol monitor proxy")
parser.add_argument("--target-host", "--th", default="127.0.0.1", help="Target debugpy host (default: 127.0.0.1)")
parser.add_argument("--target-port", "--tp", type=int, default=5678, help="Target debugpy port (default: 5678)")
parser.add_argument("--listen-port", "--lp", type=int, default=5679, help="Port to listen for VS Code (default: 5679)")
args = parser.parse_args()

monitor = DAPMonitor(
listen_port=args.listen_port,
target_host=args.target_host,
target_port=args.target_port
)
monitor.start()
8 changes: 4 additions & 4 deletions python-ecosys/debugpy/debugpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
from .common.constants import DEFAULT_HOST, DEFAULT_PORT

__all__ = [
"listen",
"wait_for_client",
"breakpoint",
"debug_this_thread",
"DEFAULT_HOST",
"DEFAULT_PORT",
"breakpoint",
"debug_this_thread",
"listen",
"wait_for_client",
]
56 changes: 30 additions & 26 deletions python-ecosys/debugpy/debugpy/common/messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@

class JsonMessageChannel:
"""Handles JSON message communication over a socket using DAP format."""

def __init__(self, sock, debug_callback=None):
self.sock = sock
self.seq = 0
self.closed = False
self._recv_buffer = b""
self._debug_print = debug_callback or (lambda x: None) # Default to no-op

def send_message(self, msg_type, command=None, **kwargs):
"""Send a DAP message."""
if self.closed:
return

self.seq += 1
message = {
"seq": self.seq,
"type": msg_type,
}

if command:
if msg_type == MSG_TYPE_REQUEST:
message["command"] = command
Expand All @@ -42,48 +42,50 @@ def send_message(self, msg_type, command=None, **kwargs):
message["event"] = command
if kwargs:
message["body"] = kwargs

json_str = json.dumps(message)
content = json_str.encode("utf-8")
header = f"Content-Length: {len(content)}\r\n\r\n".encode("utf-8")

try:
self.sock.send(header + content)
except OSError:
self.closed = True

def send_request(self, command, **kwargs):
"""Send a request message."""
self.send_message(MSG_TYPE_REQUEST, command, **kwargs)

def send_response(self, command, request_seq, success=True, body=None, message=None):
"""Send a response message."""
kwargs = {"request_seq": request_seq, "success": success}
if body is not None:
kwargs["body"] = body
if message is not None:
kwargs["message"] = message

self._debug_print(f"[DAP] SEND: response {command} (req_seq={request_seq}, success={success})")

self._debug_print(
f"[DAP] SEND: response {command} (req_seq={request_seq}, success={success})"
)
if body:
self._debug_print(f"[DAP] body: {body}")
if message:
self._debug_print(f"[DAP] message: {message}")

self.send_message(MSG_TYPE_RESPONSE, command, **kwargs)

def send_event(self, event, **kwargs):
"""Send an event message."""
self._debug_print(f"[DAP] SEND: event {event}")
if kwargs:
self._debug_print(f"[DAP] body: {kwargs}")
self.send_message(MSG_TYPE_EVENT, event, **kwargs)

def recv_message(self):
"""Receive a DAP message."""
if self.closed:
return None

try:
# Read headers
while b"\r\n\r\n" not in self._recv_buffer:
Expand All @@ -95,25 +97,25 @@ def recv_message(self):
self._recv_buffer += data
except OSError as e:
# Handle timeout and other socket errors
if hasattr(e, 'errno') and e.errno in (11, 35): # EAGAIN, EWOULDBLOCK
if hasattr(e, "errno") and e.errno in (11, 35): # EAGAIN, EWOULDBLOCK
return None # No data available
self.closed = True
return None

header_end = self._recv_buffer.find(b"\r\n\r\n")
header_str = self._recv_buffer[:header_end].decode("utf-8")
self._recv_buffer = self._recv_buffer[header_end + 4:]
self._recv_buffer = self._recv_buffer[header_end + 4 :]

# Parse Content-Length
content_length = 0
for line in header_str.split("\r\n"):
if line.startswith("Content-Length:"):
content_length = int(line.split(":", 1)[1].strip())
break

if content_length == 0:
return None

# Read body
while len(self._recv_buffer) < content_length:
try:
Expand All @@ -123,28 +125,30 @@ def recv_message(self):
return None
self._recv_buffer += data
except OSError as e:
if hasattr(e, 'errno') and e.errno in (11, 35): # EAGAIN, EWOULDBLOCK
if hasattr(e, "errno") and e.errno in (11, 35): # EAGAIN, EWOULDBLOCK
return None
self.closed = True
return None

body = self._recv_buffer[:content_length]
self._recv_buffer = self._recv_buffer[content_length:]

# Parse JSON
try:
message = json.loads(body.decode("utf-8"))
self._debug_print(f"[DAP] Successfully received message: {message.get('type')} {message.get('command', message.get('event', 'unknown'))}")
self._debug_print(
f"[DAP] Successfully received message: {message.get('type')} {message.get('command', message.get('event', 'unknown'))}"
)
return message
except (ValueError, UnicodeDecodeError) as e:
print(f"[DAP] JSON parse error: {e}")
return None

except OSError as e:
print(f"[DAP] Socket error in recv_message: {e}")
self.closed = True
return None

def close(self):
"""Close the channel."""
self.closed = True
Expand Down
Loading
Loading