kopia lustrzana https://github.com/micropython/micropython-lib
python-ecosys/debugpy: Add VS Code debugging support for MicroPython.
This implementation provides a Debug Adapter Protocol (DAP) server that enables VS Code to debug MicroPython code with full breakpoint, stepping, and variable inspection capabilities. Features: - Manual breakpoints via debugpy.breakpoint() - Line breakpoints set from VS Code - Stack trace inspection - Variable scopes (locals/globals) - Source code viewing - Stepping (into/over/out) - Non-blocking architecture for MicroPython's single-threaded environment - Conditional debug logging based on VS Code's logToFile setting Implementation highlights: - Uses MicroPython's sys.settrace() for execution monitoring - Handles path mapping between VS Code and MicroPython - Efficient O(n) fibonacci demo (was O(2^n) recursive) - Compatible with MicroPython's limited frame object attributes - Comprehensive DAP protocol support Files: - debugpy/: Core debugging implementation - test_vscode.py: VS Code integration test - VSCODE_TESTING_GUIDE.md: Setup and usage instructions - dap_monitor.py: Protocol debugging utility Usage: ```python import debugpy debugpy.listen() # Start debug server debugpy.debug_this_thread() # Enable tracing debugpy.breakpoint() # Manual breakpoint ``` 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>pull/1022/head
rodzic
6e24cffe95
commit
c98b355ef4
|
@ -0,0 +1,172 @@
|
|||
# MicroPython debugpy
|
||||
|
||||
A minimal implementation of debugpy for MicroPython, enabling remote debugging
|
||||
such as VS Code debugging support.
|
||||
|
||||
## Features
|
||||
|
||||
- Debug Adapter Protocol (DAP) support for VS Code integration
|
||||
- Basic debugging operations:
|
||||
- Breakpoints
|
||||
- Step over/into/out
|
||||
- Stack trace inspection
|
||||
- Variable inspection (globals, locals generally not supported)
|
||||
- Expression evaluation
|
||||
- Pause/continue execution
|
||||
|
||||
## Requirements
|
||||
|
||||
- MicroPython with `sys.settrace` support (enabled with `MICROPY_PY_SYS_SETTRACE`)
|
||||
- Socket support for network communication
|
||||
- JSON support for DAP message parsing
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
import debugpy
|
||||
|
||||
# Start listening for debugger connections
|
||||
host, port = debugpy.listen() # Default: 127.0.0.1:5678
|
||||
print(f"Debugger listening on {host}:{port}")
|
||||
|
||||
# Enable debugging for current thread
|
||||
debugpy.debug_this_thread()
|
||||
|
||||
# Your code here...
|
||||
def my_function():
|
||||
x = 10
|
||||
y = 20
|
||||
result = x + y # Set breakpoint here in VS Code
|
||||
return result
|
||||
|
||||
result = my_function()
|
||||
print(f"Result: {result}")
|
||||
|
||||
# Manual breakpoint
|
||||
debugpy.breakpoint()
|
||||
```
|
||||
|
||||
### VS Code Configuration
|
||||
|
||||
Create a `.vscode/launch.json` file in your project:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach to MicroPython",
|
||||
"type": "python",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 5678
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "."
|
||||
}
|
||||
],
|
||||
"justMyCode": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
1. Build the MicroPython Unix coverage port:
|
||||
```bash
|
||||
cd ports/unix
|
||||
make CFLAGS_EXTRA="-DMICROPY_PY_SYS_SETTRACE=1"
|
||||
```
|
||||
|
||||
2. Run the test script:
|
||||
```bash
|
||||
cd lib/micropython-lib/python-ecosys/debugpy
|
||||
../../../../ports/unix/build-coverage/micropython test_debugpy.py
|
||||
```
|
||||
|
||||
3. In VS Code, open the debugpy folder and press F5 to attach the debugger
|
||||
|
||||
4. Set breakpoints in the test script and observe debugging functionality
|
||||
|
||||
## API Reference
|
||||
|
||||
### `debugpy.listen(port=5678, host="127.0.0.1")`
|
||||
|
||||
Start listening for debugger connections.
|
||||
|
||||
**Parameters:**
|
||||
- `port`: Port number to listen on (default: 5678)
|
||||
- `host`: Host address to bind to (default: "127.0.0.1")
|
||||
|
||||
**Returns:** Tuple of (host, port) actually used
|
||||
|
||||
### `debugpy.debug_this_thread()`
|
||||
|
||||
Enable debugging for the current thread by installing the trace function.
|
||||
|
||||
### `debugpy.breakpoint()`
|
||||
|
||||
Trigger a manual breakpoint that will pause execution if a debugger is attached.
|
||||
|
||||
### `debugpy.wait_for_client()`
|
||||
|
||||
Wait for the debugger client to connect and initialize.
|
||||
|
||||
### `debugpy.is_client_connected()`
|
||||
|
||||
Check if a debugger client is currently connected.
|
||||
|
||||
**Returns:** Boolean indicating connection status
|
||||
|
||||
### `debugpy.disconnect()`
|
||||
|
||||
Disconnect from the debugger client and clean up resources.
|
||||
|
||||
## Architecture
|
||||
|
||||
The implementation consists of several key components:
|
||||
|
||||
1. **Public API** (`public_api.py`): Main entry points for users
|
||||
2. **Debug Session** (`server/debug_session.py`): Handles DAP protocol communication
|
||||
3. **PDB Adapter** (`server/pdb_adapter.py`): Bridges DAP and MicroPython's trace system
|
||||
4. **Messaging** (`common/messaging.py`): JSON message handling for DAP
|
||||
5. **Constants** (`common/constants.py`): DAP protocol constants
|
||||
|
||||
## Limitations
|
||||
|
||||
This is a minimal implementation with the following limitations:
|
||||
|
||||
- Single-threaded debugging only
|
||||
- No conditional breakpoints
|
||||
- No function breakpoints
|
||||
- Limited variable inspection (no nested object expansion)
|
||||
- No step back functionality
|
||||
- No hot code reloading
|
||||
- Simplified stepping implementation
|
||||
|
||||
## Compatibility
|
||||
|
||||
Tested with:
|
||||
- MicroPython Unix port
|
||||
- VS Code with Python/debugpy extension
|
||||
- CPython 3.x (for comparison)
|
||||
|
||||
## Contributing
|
||||
|
||||
This implementation provides a foundation for MicroPython debugging. Contributions are welcome to add:
|
||||
|
||||
- Conditional breakpoint support
|
||||
- Better variable inspection
|
||||
- Multi-threading support
|
||||
- Performance optimizations
|
||||
- Additional DAP features
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see the MicroPython project license for details.
|
|
@ -0,0 +1,162 @@
|
|||
#!/usr/bin/env python3
|
||||
"""DAP protocol monitor - sits between VS Code and MicroPython debugpy."""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
|
||||
class DAPMonitor:
|
||||
def __init__(self, listen_port=5679, target_host='127.0.0.1', target_port=5678):
|
||||
self.listen_port = listen_port
|
||||
self.target_host = target_host
|
||||
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 True:
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nStopping DAP Monitor...")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
self.cleanup()
|
||||
|
||||
def forward_client_to_server(self):
|
||||
"""Forward messages from VS Code client to MicroPython server."""
|
||||
try:
|
||||
while True:
|
||||
data = self.receive_dap_message(self.client_sock, "VS Code")
|
||||
if data is None:
|
||||
break
|
||||
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:
|
||||
while True:
|
||||
data = self.receive_dap_message(self.server_sock, "MicroPython")
|
||||
if data is None:
|
||||
break
|
||||
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:
|
||||
# Read headers
|
||||
header = b""
|
||||
while b"\r\n\r\n" not in header:
|
||||
byte = sock.recv(1)
|
||||
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
|
||||
|
||||
# Log the message
|
||||
try:
|
||||
message = json.loads(content.decode('utf-8'))
|
||||
msg_type = message.get('type', 'unknown')
|
||||
command = message.get('command', message.get('event', 'unknown'))
|
||||
seq = message.get('seq', 0)
|
||||
|
||||
print(f"\n[{source}] {msg_type.upper()}: {command} (seq={seq})")
|
||||
|
||||
if msg_type == 'request':
|
||||
args = message.get('arguments', {})
|
||||
if args:
|
||||
print(f" Arguments: {json.dumps(args, indent=2)}")
|
||||
elif msg_type == 'response':
|
||||
success = message.get('success', False)
|
||||
req_seq = message.get('request_seq', 0)
|
||||
print(f" Success: {success}, Request Seq: {req_seq}")
|
||||
body = message.get('body')
|
||||
if body:
|
||||
print(f" Body: {json.dumps(body, indent=2)}")
|
||||
msg = message.get('message')
|
||||
if msg:
|
||||
print(f" Message: {msg}")
|
||||
elif msg_type == 'event':
|
||||
body = message.get('body', {})
|
||||
if body:
|
||||
print(f" Body: {json.dumps(body, indent=2)}")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(f"\n[{source}] Invalid JSON: {content}")
|
||||
|
||||
return header + content
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error receiving from {source}: {e}")
|
||||
return None
|
||||
|
||||
def send_raw_data(self, sock, data):
|
||||
"""Send raw data to socket."""
|
||||
try:
|
||||
sock.send(data)
|
||||
except Exception as e:
|
||||
print(f"Error sending data: {e}")
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up sockets."""
|
||||
if self.client_sock:
|
||||
self.client_sock.close()
|
||||
if self.server_sock:
|
||||
self.server_sock.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
monitor = DAPMonitor()
|
||||
monitor.start()
|
|
@ -0,0 +1,20 @@
|
|||
"""MicroPython debugpy implementation.
|
||||
|
||||
A minimal port of debugpy for MicroPython to enable VS Code debugging support.
|
||||
This implementation focuses on the core DAP (Debug Adapter Protocol) functionality
|
||||
needed for basic debugging operations like breakpoints, stepping, and variable inspection.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
from .public_api import listen, wait_for_client, breakpoint, debug_this_thread
|
||||
from .common.constants import DEFAULT_HOST, DEFAULT_PORT
|
||||
|
||||
__all__ = [
|
||||
"listen",
|
||||
"wait_for_client",
|
||||
"breakpoint",
|
||||
"debug_this_thread",
|
||||
"DEFAULT_HOST",
|
||||
"DEFAULT_PORT",
|
||||
]
|
|
@ -0,0 +1 @@
|
|||
# Common utilities and constants for debugpy
|
|
@ -0,0 +1,60 @@
|
|||
"""Constants used throughout debugpy."""
|
||||
|
||||
# Default networking settings
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 5678
|
||||
|
||||
# DAP message types
|
||||
MSG_TYPE_REQUEST = "request"
|
||||
MSG_TYPE_RESPONSE = "response"
|
||||
MSG_TYPE_EVENT = "event"
|
||||
|
||||
# DAP events
|
||||
EVENT_INITIALIZED = "initialized"
|
||||
EVENT_STOPPED = "stopped"
|
||||
EVENT_CONTINUED = "continued"
|
||||
EVENT_THREAD = "thread"
|
||||
EVENT_BREAKPOINT = "breakpoint"
|
||||
EVENT_OUTPUT = "output"
|
||||
EVENT_TERMINATED = "terminated"
|
||||
EVENT_EXITED = "exited"
|
||||
|
||||
# DAP commands
|
||||
CMD_INITIALIZE = "initialize"
|
||||
CMD_LAUNCH = "launch"
|
||||
CMD_ATTACH = "attach"
|
||||
CMD_SET_BREAKPOINTS = "setBreakpoints"
|
||||
CMD_CONTINUE = "continue"
|
||||
CMD_NEXT = "next"
|
||||
CMD_STEP_IN = "stepIn"
|
||||
CMD_STEP_OUT = "stepOut"
|
||||
CMD_PAUSE = "pause"
|
||||
CMD_STACK_TRACE = "stackTrace"
|
||||
CMD_SCOPES = "scopes"
|
||||
CMD_VARIABLES = "variables"
|
||||
CMD_EVALUATE = "evaluate"
|
||||
CMD_DISCONNECT = "disconnect"
|
||||
CMD_CONFIGURATION_DONE = "configurationDone"
|
||||
CMD_THREADS = "threads"
|
||||
CMD_SOURCE = "source"
|
||||
|
||||
# Stop reasons
|
||||
STOP_REASON_STEP = "step"
|
||||
STOP_REASON_BREAKPOINT = "breakpoint"
|
||||
STOP_REASON_EXCEPTION = "exception"
|
||||
STOP_REASON_PAUSE = "pause"
|
||||
STOP_REASON_ENTRY = "entry"
|
||||
|
||||
# Thread reasons
|
||||
THREAD_REASON_STARTED = "started"
|
||||
THREAD_REASON_EXITED = "exited"
|
||||
|
||||
# Trace events
|
||||
TRACE_CALL = "call"
|
||||
TRACE_LINE = "line"
|
||||
TRACE_RETURN = "return"
|
||||
TRACE_EXCEPTION = "exception"
|
||||
|
||||
# Scope types
|
||||
SCOPE_LOCALS = "locals"
|
||||
SCOPE_GLOBALS = "globals"
|
|
@ -0,0 +1,154 @@
|
|||
"""JSON message handling for DAP protocol."""
|
||||
|
||||
import json
|
||||
from .constants import MSG_TYPE_REQUEST, MSG_TYPE_RESPONSE, MSG_TYPE_EVENT
|
||||
|
||||
|
||||
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
|
||||
if kwargs:
|
||||
message["arguments"] = kwargs
|
||||
elif msg_type == MSG_TYPE_RESPONSE:
|
||||
message["command"] = command
|
||||
message["request_seq"] = kwargs.get("request_seq", 0)
|
||||
message["success"] = kwargs.get("success", True)
|
||||
if "body" in kwargs:
|
||||
message["body"] = kwargs["body"]
|
||||
if "message" in kwargs:
|
||||
message["message"] = kwargs["message"]
|
||||
elif msg_type == MSG_TYPE_EVENT:
|
||||
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})")
|
||||
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:
|
||||
try:
|
||||
data = self.sock.recv(1024)
|
||||
if not data:
|
||||
self.closed = True
|
||||
return None
|
||||
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
|
||||
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:]
|
||||
|
||||
# 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:
|
||||
data = self.sock.recv(content_length - len(self._recv_buffer))
|
||||
if not data:
|
||||
self.closed = True
|
||||
return None
|
||||
self._recv_buffer += data
|
||||
except OSError as e:
|
||||
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'))}")
|
||||
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
|
||||
try:
|
||||
self.sock.close()
|
||||
except OSError:
|
||||
pass
|
|
@ -0,0 +1,126 @@
|
|||
"""Public API for debugpy."""
|
||||
|
||||
import socket
|
||||
import sys
|
||||
from .common.constants import DEFAULT_HOST, DEFAULT_PORT
|
||||
from .server.debug_session import DebugSession
|
||||
|
||||
_debug_session = None
|
||||
|
||||
|
||||
def listen(port=DEFAULT_PORT, host=DEFAULT_HOST):
|
||||
"""Start listening for debugger connections.
|
||||
|
||||
Args:
|
||||
port: Port number to listen on (default: 5678)
|
||||
host: Host address to bind to (default: "127.0.0.1")
|
||||
|
||||
Returns:
|
||||
(host, port) tuple of the actual listening address
|
||||
"""
|
||||
global _debug_session
|
||||
|
||||
if _debug_session is not None:
|
||||
raise RuntimeError("Already listening for debugger")
|
||||
|
||||
# Create listening socket
|
||||
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
except:
|
||||
pass # Not supported in MicroPython
|
||||
|
||||
# Use getaddrinfo for MicroPython compatibility
|
||||
addr_info = socket.getaddrinfo(host, port)
|
||||
addr = addr_info[0][-1] # Get the sockaddr
|
||||
listener.bind(addr)
|
||||
listener.listen(1)
|
||||
|
||||
# getsockname not available in MicroPython, use original values
|
||||
print(f"Debugpy listening on {host}:{port}")
|
||||
|
||||
# Wait for connection
|
||||
client_sock = None
|
||||
try:
|
||||
client_sock, client_addr = listener.accept()
|
||||
print(f"Debugger connected from {client_addr}")
|
||||
|
||||
# Create debug session
|
||||
_debug_session = DebugSession(client_sock)
|
||||
|
||||
# Handle just the initialize request, then return immediately
|
||||
print("[DAP] Waiting for initialize request...")
|
||||
init_message = _debug_session.channel.recv_message()
|
||||
if init_message and init_message.get('command') == 'initialize':
|
||||
_debug_session._handle_message(init_message)
|
||||
print("[DAP] Initialize request handled - returning control immediately")
|
||||
else:
|
||||
print(f"[DAP] Warning: Expected initialize, got {init_message}")
|
||||
|
||||
# Set socket to non-blocking for subsequent message processing
|
||||
_debug_session.channel.sock.settimeout(0.001)
|
||||
|
||||
print("[DAP] Debug session ready - all other messages will be handled in trace function")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DAP] Connection error: {e}")
|
||||
if client_sock:
|
||||
client_sock.close()
|
||||
_debug_session = None
|
||||
finally:
|
||||
# Only close the listener, not the client connection
|
||||
listener.close()
|
||||
|
||||
return (host, port)
|
||||
|
||||
|
||||
def wait_for_client():
|
||||
"""Wait for the debugger client to connect and initialize."""
|
||||
global _debug_session
|
||||
if _debug_session:
|
||||
_debug_session.wait_for_client()
|
||||
|
||||
|
||||
def breakpoint():
|
||||
"""Trigger a breakpoint in the debugger."""
|
||||
global _debug_session
|
||||
if _debug_session:
|
||||
_debug_session.trigger_breakpoint()
|
||||
else:
|
||||
# Fallback to built-in breakpoint if available
|
||||
if hasattr(__builtins__, 'breakpoint'):
|
||||
__builtins__.breakpoint()
|
||||
|
||||
|
||||
def debug_this_thread():
|
||||
"""Enable debugging for the current thread."""
|
||||
global _debug_session
|
||||
if _debug_session:
|
||||
_debug_session.debug_this_thread()
|
||||
else:
|
||||
# Install trace function even if no session yet
|
||||
if hasattr(sys, 'settrace'):
|
||||
sys.settrace(_default_trace_func)
|
||||
else:
|
||||
raise RuntimeError("MICROPY_PY_SYS_SETTRACE required")
|
||||
|
||||
|
||||
def _default_trace_func(frame, event, arg):
|
||||
"""Default trace function when no debug session is active."""
|
||||
# Just return None to continue execution
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def is_client_connected():
|
||||
"""Check if a debugger client is connected."""
|
||||
global _debug_session
|
||||
return _debug_session is not None and _debug_session.is_connected()
|
||||
|
||||
|
||||
def disconnect():
|
||||
"""Disconnect from the debugger client."""
|
||||
global _debug_session
|
||||
if _debug_session:
|
||||
_debug_session.disconnect()
|
||||
_debug_session = None
|
|
@ -0,0 +1 @@
|
|||
# Debug server components
|
|
@ -0,0 +1,423 @@
|
|||
"""Main debug session handling DAP protocol communication."""
|
||||
|
||||
import sys
|
||||
from ..common.messaging import JsonMessageChannel
|
||||
from ..common.constants import (
|
||||
CMD_INITIALIZE, CMD_LAUNCH, CMD_ATTACH, CMD_SET_BREAKPOINTS,
|
||||
CMD_CONTINUE, CMD_NEXT, CMD_STEP_IN, CMD_STEP_OUT, CMD_PAUSE,
|
||||
CMD_STACK_TRACE, CMD_SCOPES, CMD_VARIABLES, CMD_EVALUATE, CMD_DISCONNECT,
|
||||
CMD_CONFIGURATION_DONE, CMD_THREADS, CMD_SOURCE, EVENT_INITIALIZED, EVENT_STOPPED, EVENT_CONTINUED, EVENT_TERMINATED,
|
||||
STOP_REASON_BREAKPOINT, STOP_REASON_STEP, STOP_REASON_PAUSE,
|
||||
TRACE_CALL, TRACE_LINE, TRACE_RETURN, TRACE_EXCEPTION
|
||||
)
|
||||
from .pdb_adapter import PdbAdapter
|
||||
|
||||
|
||||
class DebugSession:
|
||||
"""Manages a debugging session with a DAP client."""
|
||||
|
||||
def __init__(self, client_socket):
|
||||
self.debug_logging = False # Initialize first
|
||||
self.channel = JsonMessageChannel(client_socket, self._debug_print)
|
||||
self.pdb = PdbAdapter()
|
||||
self.pdb._debug_session = self # Allow PDB to process messages during wait
|
||||
self.initialized = False
|
||||
self.connected = True
|
||||
self.thread_id = 1 # Simple single-thread model
|
||||
self.stepping = False
|
||||
self.paused = False
|
||||
|
||||
def _debug_print(self, message):
|
||||
"""Print debug message only if debug logging is enabled."""
|
||||
if self.debug_logging:
|
||||
print(message)
|
||||
|
||||
def start(self):
|
||||
"""Start the debug session message loop."""
|
||||
try:
|
||||
while self.connected and not self.channel.closed:
|
||||
message = self.channel.recv_message()
|
||||
if message is None:
|
||||
break
|
||||
|
||||
self._handle_message(message)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Debug session error: {e}")
|
||||
finally:
|
||||
self.disconnect()
|
||||
|
||||
def initialize_connection(self):
|
||||
"""Initialize the connection - handle just the essential initial messages then return."""
|
||||
# Note: debug_logging not available yet during init, so we always show these messages
|
||||
print("[DAP] Processing initial DAP messages...")
|
||||
|
||||
try:
|
||||
# Process initial messages quickly and return control to main thread
|
||||
# We'll handle ongoing messages in the trace function
|
||||
attached = False
|
||||
message_count = 0
|
||||
max_init_messages = 6 # Just handle the first few essential messages
|
||||
|
||||
while message_count < max_init_messages and not attached:
|
||||
try:
|
||||
# Short timeout - don't block the main thread for long
|
||||
self.channel.sock.settimeout(1.0)
|
||||
message = self.channel.recv_message()
|
||||
if message is None:
|
||||
print(f"[DAP] No more messages in initial batch")
|
||||
break
|
||||
|
||||
print(f"[DAP] Initial message #{message_count + 1}: {message.get('command')}")
|
||||
self._handle_message(message)
|
||||
message_count += 1
|
||||
|
||||
# Just wait for attach, then we can return control
|
||||
if message.get('command') == 'attach':
|
||||
attached = True
|
||||
print("[DAP] ✅ Attach received - returning control to main thread")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DAP] Exception in initial processing: {e}")
|
||||
break
|
||||
finally:
|
||||
self.channel.sock.settimeout(None)
|
||||
|
||||
# After attach, continue processing a few more messages quickly
|
||||
if attached:
|
||||
self._debug_print("[DAP] Processing remaining setup messages...")
|
||||
additional_count = 0
|
||||
while additional_count < 4: # Just a few more
|
||||
try:
|
||||
self.channel.sock.settimeout(0.5) # Short timeout
|
||||
message = self.channel.recv_message()
|
||||
if message is None:
|
||||
break
|
||||
self._debug_print(f"[DAP] Setup message: {message.get('command')}")
|
||||
self._handle_message(message)
|
||||
additional_count += 1
|
||||
except:
|
||||
break
|
||||
finally:
|
||||
self.channel.sock.settimeout(None)
|
||||
|
||||
print(f"[DAP] Initial setup complete - main thread can continue")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DAP] Initialization error: {e}")
|
||||
|
||||
def process_pending_messages(self):
|
||||
"""Process any pending DAP messages without blocking."""
|
||||
try:
|
||||
# Set socket to non-blocking mode for message processing
|
||||
self.channel.sock.settimeout(0.001) # Very short timeout
|
||||
|
||||
while True:
|
||||
message = self.channel.recv_message()
|
||||
if message is None:
|
||||
break
|
||||
self._handle_message(message)
|
||||
|
||||
except Exception:
|
||||
# No messages available or socket error
|
||||
pass
|
||||
finally:
|
||||
# Reset to blocking mode
|
||||
self.channel.sock.settimeout(None)
|
||||
|
||||
def _handle_message(self, message):
|
||||
"""Handle incoming DAP messages."""
|
||||
msg_type = message.get("type")
|
||||
command = message.get("command", message.get("event", "unknown"))
|
||||
seq = message.get("seq", 0)
|
||||
|
||||
self._debug_print(f"[DAP] RECV: {msg_type} {command} (seq={seq})")
|
||||
if message.get("arguments"):
|
||||
self._debug_print(f"[DAP] args: {message['arguments']}")
|
||||
|
||||
if msg_type == "request":
|
||||
self._handle_request(message)
|
||||
elif msg_type == "response":
|
||||
# We don't expect responses from client
|
||||
self._debug_print(f"[DAP] Unexpected response from client: {message}")
|
||||
elif msg_type == "event":
|
||||
# We don't expect events from client
|
||||
self._debug_print(f"[DAP] Unexpected event from client: {message}")
|
||||
|
||||
def _handle_request(self, message):
|
||||
"""Handle DAP request messages."""
|
||||
command = message.get("command")
|
||||
seq = message.get("seq", 0)
|
||||
args = message.get("arguments", {})
|
||||
|
||||
try:
|
||||
if command == CMD_INITIALIZE:
|
||||
self._handle_initialize(seq, args)
|
||||
elif command == CMD_LAUNCH:
|
||||
self._handle_launch(seq, args)
|
||||
elif command == CMD_ATTACH:
|
||||
self._handle_attach(seq, args)
|
||||
elif command == CMD_SET_BREAKPOINTS:
|
||||
self._handle_set_breakpoints(seq, args)
|
||||
elif command == CMD_CONTINUE:
|
||||
self._handle_continue(seq, args)
|
||||
elif command == CMD_NEXT:
|
||||
self._handle_next(seq, args)
|
||||
elif command == CMD_STEP_IN:
|
||||
self._handle_step_in(seq, args)
|
||||
elif command == CMD_STEP_OUT:
|
||||
self._handle_step_out(seq, args)
|
||||
elif command == CMD_PAUSE:
|
||||
self._handle_pause(seq, args)
|
||||
elif command == CMD_STACK_TRACE:
|
||||
self._handle_stack_trace(seq, args)
|
||||
elif command == CMD_SCOPES:
|
||||
self._handle_scopes(seq, args)
|
||||
elif command == CMD_VARIABLES:
|
||||
self._handle_variables(seq, args)
|
||||
elif command == CMD_EVALUATE:
|
||||
self._handle_evaluate(seq, args)
|
||||
elif command == CMD_DISCONNECT:
|
||||
self._handle_disconnect(seq, args)
|
||||
elif command == CMD_CONFIGURATION_DONE:
|
||||
self._handle_configuration_done(seq, args)
|
||||
elif command == CMD_THREADS:
|
||||
self._handle_threads(seq, args)
|
||||
elif command == CMD_SOURCE:
|
||||
self._handle_source(seq, args)
|
||||
else:
|
||||
self.channel.send_response(command, seq, success=False,
|
||||
message=f"Unknown command: {command}")
|
||||
|
||||
except Exception as e:
|
||||
self.channel.send_response(command, seq, success=False,
|
||||
message=str(e))
|
||||
|
||||
def _handle_initialize(self, seq, args):
|
||||
"""Handle initialize request."""
|
||||
capabilities = {
|
||||
"supportsConfigurationDoneRequest": True,
|
||||
"supportsFunctionBreakpoints": False,
|
||||
"supportsConditionalBreakpoints": False,
|
||||
"supportsHitConditionalBreakpoints": False,
|
||||
"supportsEvaluateForHovers": True,
|
||||
"supportsStepBack": False,
|
||||
"supportsSetVariable": False,
|
||||
"supportsRestartFrame": False,
|
||||
"supportsGotoTargetsRequest": False,
|
||||
"supportsStepInTargetsRequest": False,
|
||||
"supportsCompletionsRequest": False,
|
||||
"supportsModulesRequest": False,
|
||||
"additionalModuleColumns": [],
|
||||
"supportedChecksumAlgorithms": [],
|
||||
"supportsRestartRequest": False,
|
||||
"supportsExceptionOptions": False,
|
||||
"supportsValueFormattingOptions": False,
|
||||
"supportsExceptionInfoRequest": False,
|
||||
"supportTerminateDebuggee": True,
|
||||
"supportSuspendDebuggee": True,
|
||||
"supportsDelayedStackTraceLoading": False,
|
||||
"supportsLoadedSourcesRequest": False,
|
||||
"supportsLogPoints": False,
|
||||
"supportsTerminateThreadsRequest": False,
|
||||
"supportsSetExpression": False,
|
||||
"supportsTerminateRequest": True,
|
||||
"supportsDataBreakpoints": False,
|
||||
"supportsReadMemoryRequest": False,
|
||||
"supportsWriteMemoryRequest": False,
|
||||
"supportsDisassembleRequest": False,
|
||||
"supportsCancelRequest": False,
|
||||
"supportsBreakpointLocationsRequest": False,
|
||||
"supportsClipboardContext": False,
|
||||
}
|
||||
|
||||
self.channel.send_response(CMD_INITIALIZE, seq, body=capabilities)
|
||||
self.channel.send_event(EVENT_INITIALIZED)
|
||||
self.initialized = True
|
||||
|
||||
def _handle_launch(self, seq, args):
|
||||
"""Handle launch request."""
|
||||
# For attach-mode debugging, we don't need to launch anything
|
||||
self.channel.send_response(CMD_LAUNCH, seq)
|
||||
|
||||
def _handle_attach(self, seq, args):
|
||||
"""Handle attach request."""
|
||||
# Check if debug logging should be enabled
|
||||
self.debug_logging = args.get("logToFile", False)
|
||||
|
||||
self._debug_print(f"[DAP] Processing attach request with args: {args}")
|
||||
print(f"[DAP] Debug logging {'enabled' if self.debug_logging else 'disabled'} (logToFile={self.debug_logging})")
|
||||
|
||||
# Enable trace function
|
||||
self.pdb.set_trace_function(self._trace_function)
|
||||
self.channel.send_response(CMD_ATTACH, seq)
|
||||
|
||||
# After successful attach, we might need to send additional events
|
||||
# Some debuggers expect a 'process' event or thread events
|
||||
self._debug_print("[DAP] Attach completed, debugging is now active")
|
||||
|
||||
def _handle_set_breakpoints(self, seq, args):
|
||||
"""Handle setBreakpoints request."""
|
||||
source = args.get("source", {})
|
||||
filename = source.get("path", "<unknown>")
|
||||
breakpoints = args.get("breakpoints", [])
|
||||
|
||||
# Set breakpoints in pdb adapter
|
||||
actual_breakpoints = self.pdb.set_breakpoints(filename, breakpoints)
|
||||
|
||||
self.channel.send_response(CMD_SET_BREAKPOINTS, seq,
|
||||
body={"breakpoints": actual_breakpoints})
|
||||
|
||||
def _handle_continue(self, seq, args):
|
||||
"""Handle continue request."""
|
||||
self.stepping = False
|
||||
self.paused = False
|
||||
self.pdb.continue_execution()
|
||||
self.channel.send_response(CMD_CONTINUE, seq)
|
||||
|
||||
def _handle_next(self, seq, args):
|
||||
"""Handle next (step over) request."""
|
||||
self.stepping = True
|
||||
self.paused = False
|
||||
self.pdb.step_over()
|
||||
self.channel.send_response(CMD_NEXT, seq)
|
||||
|
||||
def _handle_step_in(self, seq, args):
|
||||
"""Handle stepIn request."""
|
||||
self.stepping = True
|
||||
self.paused = False
|
||||
self.pdb.step_into()
|
||||
self.channel.send_response(CMD_STEP_IN, seq)
|
||||
|
||||
def _handle_step_out(self, seq, args):
|
||||
"""Handle stepOut request."""
|
||||
self.stepping = True
|
||||
self.paused = False
|
||||
self.pdb.step_out()
|
||||
self.channel.send_response(CMD_STEP_OUT, seq)
|
||||
|
||||
def _handle_pause(self, seq, args):
|
||||
"""Handle pause request."""
|
||||
self.paused = True
|
||||
self.pdb.pause()
|
||||
self.channel.send_response(CMD_PAUSE, seq)
|
||||
|
||||
def _handle_stack_trace(self, seq, args):
|
||||
"""Handle stackTrace request."""
|
||||
stack_frames = self.pdb.get_stack_trace()
|
||||
self.channel.send_response(CMD_STACK_TRACE, seq,
|
||||
body={"stackFrames": stack_frames, "totalFrames": len(stack_frames)})
|
||||
|
||||
def _handle_scopes(self, seq, args):
|
||||
"""Handle scopes request."""
|
||||
frame_id = args.get("frameId", 0)
|
||||
self._debug_print(f"[DAP] Processing scopes request for frameId={frame_id}")
|
||||
scopes = self.pdb.get_scopes(frame_id)
|
||||
self._debug_print(f"[DAP] Generated scopes: {scopes}")
|
||||
self.channel.send_response(CMD_SCOPES, seq, body={"scopes": scopes})
|
||||
|
||||
def _handle_variables(self, seq, args):
|
||||
"""Handle variables request."""
|
||||
variables_ref = args.get("variablesReference", 0)
|
||||
variables = self.pdb.get_variables(variables_ref)
|
||||
self.channel.send_response(CMD_VARIABLES, seq, body={"variables": variables})
|
||||
|
||||
def _handle_evaluate(self, seq, args):
|
||||
"""Handle evaluate request."""
|
||||
expression = args.get("expression", "")
|
||||
frame_id = args.get("frameId")
|
||||
context = args.get("context", "watch")
|
||||
|
||||
try:
|
||||
result = self.pdb.evaluate_expression(expression, frame_id)
|
||||
self.channel.send_response(CMD_EVALUATE, seq, body={
|
||||
"result": str(result),
|
||||
"variablesReference": 0
|
||||
})
|
||||
except Exception as e:
|
||||
self.channel.send_response(CMD_EVALUATE, seq, success=False,
|
||||
message=str(e))
|
||||
|
||||
def _handle_disconnect(self, seq, args):
|
||||
"""Handle disconnect request."""
|
||||
self.channel.send_response(CMD_DISCONNECT, seq)
|
||||
self.disconnect()
|
||||
|
||||
def _handle_configuration_done(self, seq, args):
|
||||
"""Handle configurationDone request."""
|
||||
# This indicates that the client has finished configuring breakpoints
|
||||
# and is ready to start debugging
|
||||
self.channel.send_response(CMD_CONFIGURATION_DONE, seq)
|
||||
|
||||
def _handle_threads(self, seq, args):
|
||||
"""Handle threads request."""
|
||||
# MicroPython is single-threaded, so return one thread
|
||||
threads = [{
|
||||
"id": self.thread_id,
|
||||
"name": "main"
|
||||
}]
|
||||
self.channel.send_response(CMD_THREADS, seq, body={"threads": threads})
|
||||
|
||||
def _handle_source(self, seq, args):
|
||||
"""Handle source request."""
|
||||
source = args.get("source", {})
|
||||
source_path = source.get("path", "")
|
||||
|
||||
try:
|
||||
# Try to read the source file
|
||||
with open(source_path, 'r') as f:
|
||||
content = f.read()
|
||||
self.channel.send_response(CMD_SOURCE, seq, body={"content": content})
|
||||
except Exception as e:
|
||||
self.channel.send_response(CMD_SOURCE, seq, success=False,
|
||||
message=f"Could not read source: {e}")
|
||||
|
||||
def _trace_function(self, frame, event, arg):
|
||||
"""Trace function called by sys.settrace."""
|
||||
# Process any pending DAP messages frequently
|
||||
self.process_pending_messages()
|
||||
|
||||
# Handle breakpoints and stepping
|
||||
if self.pdb.should_stop(frame, event, arg):
|
||||
self._send_stopped_event(STOP_REASON_BREAKPOINT if self.pdb.hit_breakpoint else
|
||||
STOP_REASON_STEP if self.stepping else STOP_REASON_PAUSE)
|
||||
# Wait for continue command
|
||||
self.pdb.wait_for_continue()
|
||||
|
||||
return self._trace_function
|
||||
|
||||
def _send_stopped_event(self, reason):
|
||||
"""Send stopped event to client."""
|
||||
self.channel.send_event(EVENT_STOPPED,
|
||||
reason=reason,
|
||||
threadId=self.thread_id,
|
||||
allThreadsStopped=True)
|
||||
|
||||
def wait_for_client(self):
|
||||
"""Wait for client to initialize."""
|
||||
# This is a simplified version - in a real implementation
|
||||
# we might want to wait for specific initialization steps
|
||||
pass
|
||||
|
||||
def trigger_breakpoint(self):
|
||||
"""Trigger a manual breakpoint."""
|
||||
if self.initialized:
|
||||
self._send_stopped_event(STOP_REASON_BREAKPOINT)
|
||||
|
||||
def debug_this_thread(self):
|
||||
"""Enable debugging for current thread."""
|
||||
if hasattr(sys, 'settrace'):
|
||||
sys.settrace(self._trace_function)
|
||||
|
||||
def is_connected(self):
|
||||
"""Check if client is connected."""
|
||||
return self.connected and not self.channel.closed
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from client."""
|
||||
self.connected = False
|
||||
if hasattr(sys, 'settrace'):
|
||||
sys.settrace(None)
|
||||
self.pdb.cleanup()
|
||||
self.channel.close()
|
|
@ -0,0 +1,285 @@
|
|||
"""PDB adapter for integrating with MicroPython's trace system."""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from ..common.constants import (
|
||||
TRACE_CALL, TRACE_LINE, TRACE_RETURN, TRACE_EXCEPTION,
|
||||
SCOPE_LOCALS, SCOPE_GLOBALS
|
||||
)
|
||||
|
||||
|
||||
class PdbAdapter:
|
||||
"""Adapter between DAP protocol and MicroPython's sys.settrace functionality."""
|
||||
|
||||
def __init__(self):
|
||||
self.breakpoints = {} # filename -> {line_no: breakpoint_info}
|
||||
self.current_frame = None
|
||||
self.step_mode = None # None, 'over', 'into', 'out'
|
||||
self.step_frame = None
|
||||
self.step_depth = 0
|
||||
self.hit_breakpoint = False
|
||||
self.continue_event = False
|
||||
self.variables_cache = {} # frameId -> variables
|
||||
self.frame_id_counter = 1
|
||||
|
||||
def _debug_print(self, message):
|
||||
"""Print debug message only if debug logging is enabled."""
|
||||
if hasattr(self, '_debug_session') and self._debug_session.debug_logging:
|
||||
print(message)
|
||||
|
||||
def set_trace_function(self, trace_func):
|
||||
"""Install the trace function."""
|
||||
if hasattr(sys, 'settrace'):
|
||||
sys.settrace(trace_func)
|
||||
else:
|
||||
raise RuntimeError("sys.settrace not available")
|
||||
|
||||
def set_breakpoints(self, filename, breakpoints):
|
||||
"""Set breakpoints for a file."""
|
||||
self.breakpoints[filename] = {}
|
||||
actual_breakpoints = []
|
||||
|
||||
for bp in breakpoints:
|
||||
line = bp.get("line")
|
||||
if line:
|
||||
self.breakpoints[filename][line] = {
|
||||
"line": line,
|
||||
"verified": True,
|
||||
"source": {"path": filename}
|
||||
}
|
||||
actual_breakpoints.append({
|
||||
"line": line,
|
||||
"verified": True,
|
||||
"source": {"path": filename}
|
||||
})
|
||||
|
||||
return actual_breakpoints
|
||||
|
||||
def should_stop(self, frame, event, arg):
|
||||
"""Determine if execution should stop at this point."""
|
||||
self.current_frame = frame
|
||||
self.hit_breakpoint = False
|
||||
|
||||
# Get frame information
|
||||
filename = frame.f_code.co_filename
|
||||
lineno = frame.f_lineno
|
||||
|
||||
# Debug: print filename and line for debugging
|
||||
if event == TRACE_LINE and lineno in [20, 21, 22, 23, 24]: # Only log lines near our breakpoints
|
||||
self._debug_print(f"[PDB] Checking {filename}:{lineno} (event={event})")
|
||||
self._debug_print(f"[PDB] Available breakpoint files: {list(self.breakpoints.keys())}")
|
||||
|
||||
# Check for exact filename match first
|
||||
if filename in self.breakpoints:
|
||||
if lineno in self.breakpoints[filename]:
|
||||
self._debug_print(f"[PDB] HIT BREAKPOINT (exact match) at {filename}:{lineno}")
|
||||
self.hit_breakpoint = True
|
||||
return True
|
||||
|
||||
# Also try checking by basename for path mismatches
|
||||
def basename(path):
|
||||
return path.split('/')[-1] if '/' in path else path
|
||||
|
||||
file_basename = basename(filename)
|
||||
self._debug_print(f"[PDB] Fallback basename match: '{file_basename}' vs available files")
|
||||
for bp_file in self.breakpoints:
|
||||
bp_basename = basename(bp_file)
|
||||
self._debug_print(f"[PDB] Comparing '{file_basename}' == '{bp_basename}' ?")
|
||||
if bp_basename == file_basename:
|
||||
self._debug_print(f"[PDB] Basename match found! Checking line {lineno} in {list(self.breakpoints[bp_file].keys())}")
|
||||
if lineno in self.breakpoints[bp_file]:
|
||||
self._debug_print(f"[PDB] HIT BREAKPOINT (fallback basename match) at {filename}:{lineno} -> {bp_file}")
|
||||
self.hit_breakpoint = True
|
||||
return True
|
||||
|
||||
# Check stepping
|
||||
if self.step_mode == 'into':
|
||||
if event in (TRACE_CALL, TRACE_LINE):
|
||||
self.step_mode = None
|
||||
return True
|
||||
|
||||
elif self.step_mode == 'over':
|
||||
if event == TRACE_LINE and frame == self.step_frame:
|
||||
self.step_mode = None
|
||||
return True
|
||||
elif event == TRACE_RETURN and frame == self.step_frame:
|
||||
# Continue stepping in caller
|
||||
if hasattr(frame, 'f_back') and frame.f_back:
|
||||
self.step_frame = frame.f_back
|
||||
else:
|
||||
self.step_mode = None
|
||||
|
||||
elif self.step_mode == 'out':
|
||||
if event == TRACE_RETURN and frame == self.step_frame:
|
||||
self.step_mode = None
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def continue_execution(self):
|
||||
"""Continue execution."""
|
||||
self.step_mode = None
|
||||
self.continue_event = True
|
||||
|
||||
def step_over(self):
|
||||
"""Step over (next line)."""
|
||||
self.step_mode = 'over'
|
||||
self.step_frame = self.current_frame
|
||||
self.continue_event = True
|
||||
|
||||
def step_into(self):
|
||||
"""Step into function calls."""
|
||||
self.step_mode = 'into'
|
||||
self.continue_event = True
|
||||
|
||||
def step_out(self):
|
||||
"""Step out of current function."""
|
||||
self.step_mode = 'out'
|
||||
self.step_frame = self.current_frame
|
||||
self.continue_event = True
|
||||
|
||||
def pause(self):
|
||||
"""Pause execution at next opportunity."""
|
||||
# This is handled by the debug session
|
||||
pass
|
||||
|
||||
def wait_for_continue(self):
|
||||
"""Wait for continue command (simplified implementation)."""
|
||||
# In a real implementation, this would block until continue
|
||||
# For MicroPython, we'll use a simple polling approach
|
||||
self.continue_event = False
|
||||
|
||||
# Process DAP messages while waiting for continue
|
||||
self._debug_print("[PDB] Waiting for continue command...")
|
||||
while not self.continue_event:
|
||||
# Process any pending DAP messages (scopes, variables, etc.)
|
||||
if hasattr(self, '_debug_session'):
|
||||
self._debug_session.process_pending_messages()
|
||||
time.sleep(0.01)
|
||||
|
||||
def get_stack_trace(self):
|
||||
"""Get the current stack trace."""
|
||||
if not self.current_frame:
|
||||
return []
|
||||
|
||||
frames = []
|
||||
frame = self.current_frame
|
||||
frame_id = 0
|
||||
|
||||
while frame:
|
||||
filename = frame.f_code.co_filename
|
||||
name = frame.f_code.co_name
|
||||
line = frame.f_lineno
|
||||
|
||||
# Create frame info
|
||||
frames.append({
|
||||
"id": frame_id,
|
||||
"name": name,
|
||||
"source": {"path": filename},
|
||||
"line": line,
|
||||
"column": 1,
|
||||
"endLine": line,
|
||||
"endColumn": 1
|
||||
})
|
||||
|
||||
# Cache frame for variable access
|
||||
self.variables_cache[frame_id] = frame
|
||||
|
||||
# MicroPython doesn't have f_back attribute
|
||||
if hasattr(frame, 'f_back'):
|
||||
frame = frame.f_back
|
||||
else:
|
||||
# Only return the current frame for MicroPython
|
||||
break
|
||||
frame_id += 1
|
||||
|
||||
return frames
|
||||
|
||||
def get_scopes(self, frame_id):
|
||||
"""Get variable scopes for a frame."""
|
||||
scopes = [
|
||||
{
|
||||
"name": "Locals",
|
||||
"variablesReference": frame_id * 1000 + 1,
|
||||
"expensive": False
|
||||
},
|
||||
{
|
||||
"name": "Globals",
|
||||
"variablesReference": frame_id * 1000 + 2,
|
||||
"expensive": False
|
||||
}
|
||||
]
|
||||
return scopes
|
||||
|
||||
def get_variables(self, variables_ref):
|
||||
"""Get variables for a scope."""
|
||||
frame_id = variables_ref // 1000
|
||||
scope_type = variables_ref % 1000
|
||||
|
||||
if frame_id not in self.variables_cache:
|
||||
return []
|
||||
|
||||
frame = self.variables_cache[frame_id]
|
||||
variables = []
|
||||
|
||||
if scope_type == 1: # Locals
|
||||
var_dict = frame.f_locals if hasattr(frame, 'f_locals') else {}
|
||||
elif scope_type == 2: # Globals
|
||||
var_dict = frame.f_globals if hasattr(frame, 'f_globals') else {}
|
||||
else:
|
||||
return []
|
||||
|
||||
for name, value in var_dict.items():
|
||||
# Skip private/internal variables
|
||||
if name.startswith('__') and name.endswith('__'):
|
||||
continue
|
||||
|
||||
try:
|
||||
value_str = str(value)
|
||||
type_str = type(value).__name__
|
||||
|
||||
variables.append({
|
||||
"name": name,
|
||||
"value": value_str,
|
||||
"type": type_str,
|
||||
"variablesReference": 0 # Simple implementation - no nested objects
|
||||
})
|
||||
except Exception:
|
||||
variables.append({
|
||||
"name": name,
|
||||
"value": "<error>",
|
||||
"type": "unknown",
|
||||
"variablesReference": 0
|
||||
})
|
||||
|
||||
return variables
|
||||
|
||||
def evaluate_expression(self, expression, frame_id=None):
|
||||
"""Evaluate an expression in the context of a frame."""
|
||||
if frame_id is not None and frame_id in self.variables_cache:
|
||||
frame = self.variables_cache[frame_id]
|
||||
globals_dict = frame.f_globals if hasattr(frame, 'f_globals') else {}
|
||||
locals_dict = frame.f_locals if hasattr(frame, 'f_locals') else {}
|
||||
else:
|
||||
# Use current frame
|
||||
frame = self.current_frame
|
||||
if frame:
|
||||
globals_dict = frame.f_globals if hasattr(frame, 'f_globals') else {}
|
||||
locals_dict = frame.f_locals if hasattr(frame, 'f_locals') else {}
|
||||
else:
|
||||
globals_dict = globals()
|
||||
locals_dict = {}
|
||||
|
||||
try:
|
||||
# Evaluate the expression
|
||||
result = eval(expression, globals_dict, locals_dict)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise Exception(f"Evaluation error: {e}")
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up resources."""
|
||||
self.variables_cache.clear()
|
||||
self.breakpoints.clear()
|
||||
if hasattr(sys, 'settrace'):
|
||||
sys.settrace(None)
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Simple demo of MicroPython debugpy functionality."""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
import debugpy
|
||||
|
||||
def simple_function(a, b):
|
||||
"""A simple function to demonstrate debugging."""
|
||||
result = a + b
|
||||
print(f"Computing {a} + {b} = {result}")
|
||||
return result
|
||||
|
||||
def main():
|
||||
print("MicroPython debugpy Demo")
|
||||
print("========================")
|
||||
print()
|
||||
|
||||
# Demonstrate trace functionality
|
||||
print("1. Testing trace functionality:")
|
||||
|
||||
def trace_function(frame, event, arg):
|
||||
if event == 'call':
|
||||
print(f" -> Entering function: {frame.f_code.co_name}")
|
||||
elif event == 'line':
|
||||
print(f" -> Executing line {frame.f_lineno} in {frame.f_code.co_name}")
|
||||
elif event == 'return':
|
||||
print(f" -> Returning from {frame.f_code.co_name} with value: {arg}")
|
||||
return trace_function
|
||||
|
||||
# Enable tracing
|
||||
sys.settrace(trace_function)
|
||||
|
||||
# Execute traced function
|
||||
result = simple_function(5, 3)
|
||||
|
||||
# Disable tracing
|
||||
sys.settrace(None)
|
||||
|
||||
print(f"Result: {result}")
|
||||
print()
|
||||
|
||||
# Demonstrate debugpy components
|
||||
print("2. Testing debugpy components:")
|
||||
|
||||
# Test PDB adapter
|
||||
from debugpy.server.pdb_adapter import PdbAdapter
|
||||
pdb = PdbAdapter()
|
||||
|
||||
# Set some mock breakpoints
|
||||
breakpoints = pdb.set_breakpoints("demo.py", [{"line": 10}, {"line": 15}])
|
||||
print(f" Set breakpoints: {len(breakpoints)} breakpoints")
|
||||
|
||||
# Test messaging
|
||||
from debugpy.common.messaging import JsonMessageChannel
|
||||
print(" JsonMessageChannel available")
|
||||
|
||||
print()
|
||||
print("3. debugpy is ready for VS Code integration!")
|
||||
print(" To use with VS Code:")
|
||||
print(" - Import debugpy in your script")
|
||||
print(" - Call debugpy.listen() to start the debug server")
|
||||
print(" - Connect VS Code using the 'Attach to MicroPython' configuration")
|
||||
print(" - Set breakpoints and debug normally")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,84 @@
|
|||
# Debugging MicroPython debugpy with VS Code
|
||||
|
||||
## Method 1: Direct Connection with Enhanced Logging
|
||||
|
||||
1. **Start MicroPython with enhanced logging:**
|
||||
```bash
|
||||
~/micropython2/ports/unix/build-standard/micropython test_vscode.py
|
||||
```
|
||||
|
||||
This will now show detailed DAP protocol messages like:
|
||||
```
|
||||
[DAP] RECV: request initialize (seq=1)
|
||||
[DAP] args: {...}
|
||||
[DAP] SEND: response initialize (req_seq=1, success=True)
|
||||
```
|
||||
|
||||
2. **Connect VS Code debugger:**
|
||||
- Use the launch configuration in `.vscode/launch.json`
|
||||
- Or manually attach to `127.0.0.1:5678`
|
||||
|
||||
3. **Look for issues in the terminal output** - you'll see all DAP message exchanges
|
||||
|
||||
## Method 2: Using DAP Monitor (Recommended for detailed analysis)
|
||||
|
||||
1. **Start MicroPython debugpy server:**
|
||||
```bash
|
||||
~/micropython2/ports/unix/build-standard/micropython test_vscode.py
|
||||
```
|
||||
|
||||
2. **In another terminal, start the DAP monitor:**
|
||||
```bash
|
||||
python3 dap_monitor.py
|
||||
```
|
||||
|
||||
The monitor listens on port 5679 and forwards to port 5678
|
||||
|
||||
3. **Connect VS Code to the monitor:**
|
||||
- Modify your VS Code launch config to connect to port `5679` instead of `5678`
|
||||
- Or create a new launch config:
|
||||
```json
|
||||
{
|
||||
"name": "Debug via Monitor",
|
||||
"type": "python",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 5679
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Analyze the complete DAP conversation** in the monitor terminal
|
||||
|
||||
## VS Code Debug Logging
|
||||
|
||||
Enable VS Code's built-in DAP logging:
|
||||
|
||||
1. **Open VS Code settings** (Ctrl+,)
|
||||
2. **Search for:** `debug.console.verbosity`
|
||||
3. **Set to:** `verbose`
|
||||
4. **Also set:** `debug.allowBreakpointsEverywhere` to `true`
|
||||
|
||||
## Common Issues to Look For
|
||||
|
||||
1. **Missing required DAP capabilities** - check the `initialize` response
|
||||
2. **Breakpoint verification failures** - look for `setBreakpoints` exchanges
|
||||
3. **Thread/stack frame issues** - check `stackTrace` and `scopes` responses
|
||||
4. **Evaluation problems** - monitor `evaluate` request/response pairs
|
||||
|
||||
## Expected DAP Sequence
|
||||
|
||||
A successful debug session should show this sequence:
|
||||
|
||||
1. `initialize` request → response with capabilities
|
||||
2. `initialized` event
|
||||
3. `setBreakpoints` request → response with verified breakpoints
|
||||
4. `configurationDone` request → response
|
||||
5. `attach` request → response
|
||||
6. When execution hits breakpoint: `stopped` event
|
||||
7. `stackTrace` request → response with frames
|
||||
8. `scopes` request → response with local/global scopes
|
||||
9. `continue` request → response to resume
|
||||
|
||||
If any step fails or is missing, that's where the issue lies.
|
|
@ -0,0 +1,6 @@
|
|||
metadata(
|
||||
description="MicroPython implementation of debugpy for remote debugging",
|
||||
version="0.1.0",
|
||||
)
|
||||
|
||||
package("debugpy")
|
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test script for VS Code debugging with MicroPython debugpy."""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
import debugpy
|
||||
|
||||
def fibonacci(n):
|
||||
"""Calculate fibonacci number (iterative for efficiency)."""
|
||||
if n <= 1:
|
||||
return n
|
||||
a, b = 0, 1
|
||||
for _ in range(2, n + 1):
|
||||
a, b = b, a + b
|
||||
return b
|
||||
|
||||
def debuggable_code():
|
||||
"""The actual code we want to debug - wrapped in a function so sys.settrace will trace it."""
|
||||
print("Starting debuggable code...")
|
||||
|
||||
# Test data - set breakpoint here (using smaller numbers to avoid slow fibonacci)
|
||||
numbers = [3, 4, 5]
|
||||
for i, num in enumerate(numbers):
|
||||
print(f"Calculating fibonacci({num})...")
|
||||
result = fibonacci(num) # <-- SET BREAKPOINT HERE (line 26)
|
||||
print(f"fibonacci({num}) = {result}")
|
||||
print(sys.implementation)
|
||||
import machine
|
||||
print(dir(machine))
|
||||
|
||||
# Test manual breakpoint
|
||||
print("\nTriggering manual breakpoint...")
|
||||
debugpy.breakpoint()
|
||||
print("Manual breakpoint triggered!")
|
||||
|
||||
print("Test completed successfully!")
|
||||
|
||||
def main():
|
||||
print("MicroPython VS Code Debugging Test")
|
||||
print("==================================")
|
||||
|
||||
# Start debug server
|
||||
try:
|
||||
debugpy.listen()
|
||||
print("Debug server attached on 127.0.0.1:5678")
|
||||
print("Connecting back to VS Code debugger now...")
|
||||
# print("Set a breakpoint on line 26: 'result = fibonacci(num)'")
|
||||
# print("Press Enter to continue after connecting debugger...")
|
||||
# try:
|
||||
# input()
|
||||
# except:
|
||||
# pass
|
||||
|
||||
# Enable debugging for this thread
|
||||
debugpy.debug_this_thread()
|
||||
|
||||
# Give VS Code a moment to set breakpoints after attach
|
||||
print("\nGiving VS Code time to set breakpoints...")
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
# Call the debuggable code function so it gets traced
|
||||
debuggable_code()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nTest interrupted by user")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Micropython Attach",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "localhost",
|
||||
"port": 5678
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/lib/micropython-lib/python-ecosys/debugpy",
|
||||
"remoteRoot": "."
|
||||
}
|
||||
],
|
||||
// "logToFile": true,
|
||||
"justMyCode": false
|
||||
}
|
||||
]
|
||||
}
|
Ładowanie…
Reference in New Issue