@@ -66,6 +66,7 @@ public sealed class DapDebugger : RunnerService, IDapDebugger
6666
6767 // Dev Tunnel relay host for remote debugging
6868 private TunnelRelayTunnelHost _tunnelRelayHost ;
69+ private IWebSocketDapBridge _webSocketBridge ;
6970
7071 // Cancellation source for the connection loop, cancelled in StopAsync
7172 // so AcceptTcpClientAsync unblocks cleanly without relying on listener disposal.
@@ -74,6 +75,10 @@ public sealed class DapDebugger : RunnerService, IDapDebugger
7475 // When true, skip tunnel relay startup (unit tests only)
7576 internal bool SkipTunnelRelay { get ; set ; }
7677
78+ // When true, skip the public websocket bridge and expose the raw DAP
79+ // listener directly on the configured tunnel port (unit tests only).
80+ internal bool SkipWebSocketBridge { get ; set ; }
81+
7782 // Synchronization for step execution
7883 private TaskCompletionSource < DapCommand > _commandTcs ;
7984 private readonly object _stateLock = new object ( ) ;
@@ -108,6 +113,7 @@ public sealed class DapDebugger : RunnerService, IDapDebugger
108113 _state == DapSessionState . Running ;
109114
110115 internal DapSessionState State => _state ;
116+ internal int InternalDapPort => ( _listener ? . LocalEndpoint as IPEndPoint ) ? . Port ?? 0 ;
111117
112118 public override void Initialize ( IHostContext hostContext )
113119 {
@@ -133,9 +139,19 @@ public async Task StartAsync(IExecutionContext jobContext)
133139 _jobContext = jobContext ;
134140 _readyTcs = new TaskCompletionSource < bool > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
135141
136- _listener = new TcpListener ( IPAddress . Loopback , debuggerConfig . Tunnel . Port ) ;
142+ var dapPort = SkipWebSocketBridge ? debuggerConfig . Tunnel . Port : 0 ;
143+ _listener = new TcpListener ( IPAddress . Loopback , dapPort ) ;
137144 _listener . Start ( ) ;
138- Trace . Info ( $ "DAP debugger listening on { _listener . LocalEndpoint } ") ;
145+ if ( SkipWebSocketBridge )
146+ {
147+ Trace . Info ( $ "DAP debugger listening on { _listener . LocalEndpoint } ") ;
148+ }
149+ else
150+ {
151+ Trace . Info ( $ "Internal DAP debugger listening on { _listener . LocalEndpoint } ") ;
152+ _webSocketBridge = HostContext . CreateService < IWebSocketDapBridge > ( ) ;
153+ _webSocketBridge . Start ( debuggerConfig . Tunnel . Port , InternalDapPort ) ;
154+ }
139155
140156 // Start Dev Tunnel relay so remote clients reach the local DAP port.
141157 // The relay is torn down explicitly in StopAsync (after the DAP session
@@ -274,6 +290,25 @@ public async Task StopAsync()
274290 _tunnelRelayHost = null ;
275291 }
276292
293+ if ( _webSocketBridge != null )
294+ {
295+ Trace . Info ( "Stopping WebSocket DAP bridge" ) ;
296+ var shutdownTask = _webSocketBridge . ShutdownAsync ( ) ;
297+ if ( await Task . WhenAny ( shutdownTask , Task . Delay ( 5_000 ) ) != shutdownTask )
298+ {
299+ Trace . Warning ( "WebSocket DAP bridge shutdown timed out after 5s" ) ;
300+ _ = shutdownTask . ContinueWith (
301+ t => Trace . Error ( $ "WebSocket DAP bridge shutdown faulted: { t . Exception ? . GetBaseException ( ) . Message } ") ,
302+ TaskContinuationOptions . OnlyOnFaulted ) ;
303+ }
304+ else
305+ {
306+ Trace . Info ( "WebSocket DAP bridge stopped" ) ;
307+ }
308+
309+ _webSocketBridge = null ;
310+ }
311+
277312 CleanupConnection ( ) ;
278313
279314 // Cancel the connection loop first so AcceptTcpClientAsync unblocks
@@ -315,6 +350,7 @@ public async Task StopAsync()
315350 _connectionLoopTask = null ;
316351 _loopCts ? . Dispose ( ) ;
317352 _loopCts = null ;
353+ _webSocketBridge = null ;
318354 }
319355
320356 public async Task OnStepStartingAsync ( IStep step )
0 commit comments