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