Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a WebSocket-to-TCP bridge in the DAP subsystem so remote debugging can connect via ws:///wss:// to the runner’s DAP server (e.g., through Dev Tunnels using GitHub auth), while the internal DAP server continues to speak DAP-over-TCP.
Changes:
- Add
WebSocketDapBridgeto translate WebSocket text frames ↔ DAP TCP (Content-Length framed) messages. - Update
DapDebuggerstartup/shutdown to expose the tunnel port via the WebSocket bridge and move the internal DAP listener to an ephemeral local port. - Add L0 coverage for the bridge and for DapDebugger WebSocket connectivity (including “pre-upgraded” WebSocket streams).
Show a summary per file
| File | Description |
|---|---|
| src/Runner.Worker/Dap/WebSocketDapBridge.cs | New bridge implementation handling upgrade detection, handshake, and bidirectional message pumping. |
| src/Runner.Worker/Dap/DapDebugger.cs | Starts/stops the bridge and uses an internal ephemeral TCP port for the actual DAP server when the bridge is enabled. |
| src/Test/L0/Worker/WebSocketDapBridgeL0.cs | New L0 tests validating forwarding, rejection paths, message size limits, and disposal behavior. |
| src/Test/L0/Worker/DapDebuggerL0.cs | Extends L0 tests to validate DapDebugger can accept DAP initialize via WebSocket and pre-upgraded WebSocket streams. |
Copilot's findings
- Files reviewed: 4/4 changed files
- Comments generated: 7
src/Runner.Worker/Dap/DapDebugger.cs
Outdated
| else | ||
| { | ||
| Trace.Info($"Internal DAP debugger listening on {_listener.LocalEndpoint}"); | ||
| _webSocketBridge = new WebSocketDapBridge(); |
There was a problem hiding this comment.
you can do HostContext.CreateService<IWebSocketDapBridge>() which will create you new instance everyone instead of returned the signleton created via HostContext.GetService
There was a problem hiding this comment.
is there any existing library doing this?
it feels like we are writing too much code. 🤔
There was a problem hiding this comment.
Here is the reply i got from copilot. the watsonwebserver and System.IO.Pipelines are interesting points.
Here's a thorough analysis of the WebSocketDapBridge.cs implementation and what public libraries could replace parts of it.
What the File Does
WebSocketDapBridge.cs implements a WebSocket ↔ TCP proxy specifically for the Debug Adapter Protocol (DAP). It:
- Listens on a TCP port for incoming connections
- Performs the WebSocket HTTP Upgrade handshake manually (RFC 6455)
- Detects pre-upgraded WebSocket streams (e.g. from Dev Tunnels that already stripped the HTTP layer)
- Pumps frames bidirectionally: WS frames → DAP
Content-Length-framed TCP messages, and vice versa
What Could Be Replaced by Public Libraries
✅ WebSocket Accept Handshake (the largest custom section)
The most substantial custom code is the HTTP Upgrade handshake (~150 lines): header parsing, Sec-WebSocket-Accept computation, rejection responses.
This can be fully replaced by Microsoft.AspNetCore.WebSockets, specifically via a minimal Kestrel host or even via HttpListener-based upgrade:
// With a Kestrel/minimal API host — the handshake becomes 1 line:
await context.WebSockets.AcceptWebSocketAsync();However, the project intentionally avoids ASP.NET Core (this is Runner.Worker, a lean console process), and WebSocket.CreateFromStream (which they already use at the bottom of the handshake) is BCL, not ASP.NET. So the handshake must be done manually — or via a library.
Best drop-in alternative: WatsonWebsocket or Fleck — both handle the server-side WS handshake without requiring ASP.NET Core.
✅ ReplayableStream (prefix-replay buffer)
This is a small custom Stream subclass that replays the first 4 bytes after they've been peeked to detect the protocol. This pattern is sometimes called a "peekable" or "pre-read" stream.
Libraries with this built-in:
System.IO.Pipelines(BCL, .NET 6+) —PipeReadersupports peeking/examining bytes before consuming, eliminating the need for a customReplayableStreamentirely. This is likely the most idiomatic .NET replacement.
✅ DAP Content-Length Message Framing (TryParseDapMessage)
The DAP header parsing loop is straightforward but custom. There is no widely-used dedicated NuGet package for DAP framing, but:
Microsoft.VisualStudio.Shared.VSCodeDebugProtocolorOmniSharp/omnisharp-roslyninclude DAP protocol serializers, though not low-level TCP framing.System.IO.Pipelinesagain —SequenceReader<byte>makes the header scanning loop cleaner and allocation-free.
✅ WS→TCP Pumping (PumpWebSocketToTcpAsync / PumpTcpToWebSocketAsync)
This is generic bidirectional stream proxying. The current implementation uses List<byte> as a buffer, which is allocation-heavy.
Better alternatives:
System.IO.Pipelines— avoidsList<byte>andToArray()allocations entirely; this is what ASP.NET Core itself uses internally for WebSocket proxying.YARP(Yet Another Reverse Proxy) — overkill here, but it does contain robust WebSocket forwarding middleware.
Summary Table
Section of WebSocketDapBridge.cs |
Publicly Available Alternative |
|---|---|
| HTTP Upgrade handshake parsing | Fleck, WatsonWebsocket, or Microsoft.AspNetCore.WebSockets (needs Kestrel) |
WebSocket.CreateFromStream |
✅ Already uses BCL (built into .NET 6+, no library needed) |
ReplayableStream (prefix peek) |
System.IO.Pipelines PipeReader.AdvanceTo |
DAP Content-Length framing |
System.IO.Pipelines + SequenceReader<byte> |
| Bidirectional WS↔TCP pump | System.IO.Pipelines, or YARP for a full proxy scenario |
Protocol sniffing (ClassifyIncomingStreamPrefix) |
No public library — keep as-is, this is custom to the Dev Tunnels use case |
Practical Recommendation
The most impactful and well-scoped replacement would be:
-
Replace
ReplayableStream+ the pump'sList<byte>buffer withSystem.IO.Pipelines— it's a BCL package, zero external dependency risk, and would make the DAP framing and WS pumping both faster and allocation-free. -
Keep the handshake manual OR adopt
Fleck/WatsonWebsocket— but adopting a third-party WS library adds a dependency to a security-sensitive component (a runner handling GitHub credentials), so the manual implementation is arguably the safer call here. -
The protocol sniffing logic (
ClassifyIncomingStreamPrefix) is unique to the Dev Tunnels pre-upgraded stream scenario and has no public library equivalent — it should stay custom.
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
This adds a bridge converting messages to/from TCP <-> WS so that we can connect to the DAP server through
wss://directly and using GitHub credentials out of the box with Dev Tunnels.https://github.com/github/c2c-actions/issues/9831