Skip to content

Debugpy/updates 02 #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: add-debugpy-support
Choose a base branch
from
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