The shared Debug Adapter Protocol (DAP) implementation used by Visual Studio, Visual Studio Code, and Rider for Meadow debugging. This repository contains everything needed to deploy and debug Meadow applications, regardless of which IDE you're using.
Before DAP, each IDE had its own debugging implementation. VSCode did debugging one way, Visual Studio did it differently, and Rider had yet another approach. This meant:
- Bugs fixed in one IDE weren't fixed in the others
- Adding new debugging features required updating three separate codebases
- The team spent more time maintaining IDE integrations than improving the debugging experience
By standardizing on DAP, we now maintain a single debugging implementation. When a bug is fixed, it's fixed for all three IDEs simultaneously. When a feature is added, all users get it at the same time. This is a significant win for consistency and maintainability.
The debugging system is split into three main projects:
Contains the core functionality for communicating with Meadow devices and managing deployment. This includes:
- Device connection management
- Binary deployment and file transfer
- Runtime state management
- Device communication protocols
This layer handles the "hardware side" of debugging. It doesn't know anything about IDEs or debug protocols. It just knows how to talk to a Meadow device.
Implements the Debug Adapter Protocol server and the Meadow-specific debug session. This is where most of the "what does a debug session do?" logic lives:
- MeadowDebugSession - The main DAP command handler
- SoftDebuggerAdapter - Wraps the Mono.Debugging soft debugger and exposes it as IDebugger
- Protocol classes - DAP request and response types
- Event emitters - Convert internal events to DAP events
The DAP layer is IDE-agnostic. It implements the protocol that any IDE can speak. It doesn't care whether the client is VSCode, Visual Studio, or Rider. It just receives DAP commands, processes them, and sends back events.
The console application (meadow-debugging.exe) that IDEs actually launch. This is the bridge between the IDE's debug protocol handler and our DAP implementation:
- Reads DAP requests from stdin
- Parses JSON protocol messages
- Invokes MeadowDebugSession methods
- Writes DAP responses to stdout
When an IDE needs to debug a Meadow app, it launches this executable as a subprocess and communicates with it through stdin/stdout using JSON messages.
When a user clicks the debug button in their IDE, here's the flow:
- IDE extension prepares launch configuration (project path, serial port, etc.)
- IDE launches meadow-debugging.exe as a subprocess
- IDE sends a DAP initialize request
- IDE sends a DAP launch request with the configuration
- meadow-debugging.exe (our Host) parses the request and calls MeadowDebugSession.Launch()
- MeadowDebugSession creates a MeadowDeployer to handle deployment to the device
- Once deployed, it connects to the device's Mono debugger on the debug port
- IDE receives debugging events (stopped, breakpoint hit, thread started, etc.)
- IDE shows these events in the debug UI (variables, call stack, etc.)
- User sets breakpoints, steps through code, inspects variables
- When user clicks stop, IDE sends a DAP disconnect request
- meadow-debugging.exe calls MeadowDebugSession.Disconnect()
- Disconnect resumes the device, closes connections, and returns immediately
- meadow-debugging.exe exits cleanly
The key insight is that the IDE doesn't talk to the device. The IDE talks to our adapter, which talks to the device.
One thing to be aware of when implementing your own IDE extension: the Disconnect() method must return immediately. Don't wait for cleanup to finish in the Disconnect() handler.
Here's why: IDEs have their own timeouts for how long they'll wait for a disconnect response. If your cleanup takes longer than that timeout (usually 3-5 seconds), the IDE will forcibly kill your adapter process mid-operation. This can leave the device in an unstable state.
The correct pattern is:
public override async Task Disconnect(Response response, dynamic arguments)
{
// Send response immediately to unblock the IDE
SendResponse(response);
// Then do cleanup asynchronously in background
_ = Task.Run(async () => {
try {
// All your cleanup here - resume device, close connections, etc.
}
catch (Exception ex) {
Log($"Cleanup error: {ex.Message}");
}
});
}This ensures the IDE gets its response fast and can shut down cleanly, while cleanup continues in the background. The device still gets resumed properly, but it happens after the IDE is no longer waiting.
A debug session goes through these stages:
- Initialize - IDE and adapter exchange capabilities
- Launch - Deployment and debug connection established
- Running - IDE receives events, user can pause/step
- Stopped - Breakpoint hit, step completed, exception thrown
- Disconnect - Cleanup and shutdown
The Launch and Disconnect stages are the most complex. Launch must set up the device connection without blocking the IDE. Disconnect must clean up without blocking the IDE.
The adapter emits events back to the IDE through the DAP protocol. The main ones are:
- Stopped - A thread hit a breakpoint or has stepped
- Started - A new thread has started on the device
- Exited - A thread has exited
- Terminated - The entire debugging session has ended
- Output - Text output from the device
The IDE uses these events to update its UI. When a user sees a debugger stopped at a breakpoint, that's an event coming from this adapter.
To build the solution:
dotnet build Meadow.Debugging.sln
The Host project produces meadow-debugging.exe, which is what gets bundled into each IDE extension/plugin.
If you're building a new IDE extension for Meadow debugging, here's what you need to know:
Every IDE has a way to integrate with external debuggers. VSCode uses the Debug Adapter Protocol. Visual Studio has its own Debug Engine interface. Rider uses the IntelliJ debug system. Familiarize yourself with how your IDE launches and communicates with debuggers.
Your IDE extension needs to gather information about what to debug:
- Project path
- Target device serial port
- Build configuration (Debug/Release)
- Output path where binaries are built
- Debug port for the Mono soft debugger
Create an MSBuild property file or similar that the adapter can read. The adapter needs to know where these build outputs are.
Launch meadow-debugging.exe as a subprocess with appropriate arguments. The exact invocation depends on your IDE's API, but it should be something like:
meadow-debugging.exe --trace
The --trace flag enables diagnostic tracing. You can add --log-file if you need file logging for troubleshooting during development.
Send DAP protocol messages to the adapter's stdin and read responses from stdout. The messages are newline-delimited JSON. Each message has a sequence number and type.
Example initialize request:
{"seq":1,"type":"request","command":"initialize","arguments":{"clientID":"myide","clientName":"My IDE"}}The adapter will respond with a response message and eventually an initialized event.
Handle the events coming back from the adapter and update your IDE's debug UI. This is where you hook into your IDE's debugger UI framework (breakpoints, call stacks, variables, etc.).
When the user stops debugging, don't wait for all cleanup to finish before returning control to the IDE. Send the response immediately and let cleanup happen in the background. This is critical for a good user experience.
If you need to add new functionality to the debugging system:
- Core device communication changes go in Meadow.Debugging.Core
- DAP protocol or session changes go in Meadow.Debugging.DAP
- Host application changes go in Meadow.Debugging.Host
The adapter is designed to be IDE-agnostic. If your change is specifically for one IDE's UI, that belongs in the IDE extension, not here.
The best way to test is to actually use the debugger with each IDE:
- VSCode extension testing uses the sandboxed VSCode instance
- VS2022 testing uses the experimental Hive registry
- Rider testing uses the sandboxed Rider instance
For unit testing the core deployment and debugging logic, add tests to the Core project.
This usually means the Disconnect() cleanup didn't complete. Check:
- Is Disconnect() being called at all? Add logging to verify.
- Is the device being resumed with Continue()? This is critical.
- Is cleanup happening asynchronously without blocking? Don't wait in Disconnect().
The IDE's timeout waiting for the adapter to respond during launch. This could be:
- Deployment taking too long - optimize file transfer in Meadow.Debugging.Core
- Device connection taking too long - check network/serial connection quality
- Mono debugger connection taking too long - verify the debug port is correct
- Is the MSBuild property file being found and parsed correctly?
- Are the binary files actually deployed to the device?
- Is the Mono soft debugger connected on the right debug port?
Add logging to MeadowDebugSession and SoftDebuggerAdapter to trace the problem.
Released under the Apache 2 license.
When contributing to the adapter:
- Maintain IDE-agnostic code in the core projects
- Add comprehensive logging for troubleshooting
- Test with all three IDEs before submitting changes
- Remember: a fix here benefits users of all three IDEs