Skip to content
42 changes: 40 additions & 2 deletions src/Runner.Worker/Dap/DapDebugger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public sealed class DapDebugger : RunnerService, IDapDebugger

// Dev Tunnel relay host for remote debugging
private TunnelRelayTunnelHost _tunnelRelayHost;
private WebSocketDapBridge _webSocketBridge;

// Cancellation source for the connection loop, cancelled in StopAsync
// so AcceptTcpClientAsync unblocks cleanly without relying on listener disposal.
Expand All @@ -74,6 +75,10 @@ public sealed class DapDebugger : RunnerService, IDapDebugger
// When true, skip tunnel relay startup (unit tests only)
internal bool SkipTunnelRelay { get; set; }

// When true, skip the public websocket bridge and expose the raw DAP
// listener directly on the configured tunnel port (unit tests only).
internal bool SkipWebSocketBridge { get; set; }

// Synchronization for step execution
private TaskCompletionSource<DapCommand> _commandTcs;
private readonly object _stateLock = new object();
Expand Down Expand Up @@ -108,6 +113,7 @@ public sealed class DapDebugger : RunnerService, IDapDebugger
_state == DapSessionState.Running;

internal DapSessionState State => _state;
internal int InternalDapPort => (_listener?.LocalEndpoint as IPEndPoint)?.Port ?? 0;

public override void Initialize(IHostContext hostContext)
{
Expand All @@ -133,9 +139,21 @@ public async Task StartAsync(IExecutionContext jobContext)
_jobContext = jobContext;
_readyTcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

_listener = new TcpListener(IPAddress.Loopback, debuggerConfig.Tunnel.Port);
var dapPort = SkipWebSocketBridge ? debuggerConfig.Tunnel.Port : 0;
_listener = new TcpListener(IPAddress.Loopback, dapPort);
_listener.Start();
Trace.Info($"DAP debugger listening on {_listener.LocalEndpoint}");
if (SkipWebSocketBridge)
{
Trace.Info($"DAP debugger listening on {_listener.LocalEndpoint}");
}
else
{
Trace.Info($"Internal DAP debugger listening on {_listener.LocalEndpoint}");
_webSocketBridge = new WebSocketDapBridge();
_webSocketBridge.Initialize(HostContext);
_webSocketBridge.Configure(debuggerConfig.Tunnel.Port, InternalDapPort);
_webSocketBridge.Start();
}

// Start Dev Tunnel relay so remote clients reach the local DAP port.
// The relay is torn down explicitly in StopAsync (after the DAP session
Expand Down Expand Up @@ -274,6 +292,25 @@ public async Task StopAsync()
_tunnelRelayHost = null;
}

if (_webSocketBridge != null)
{
Trace.Info("Stopping WebSocket DAP bridge");
var shutdownTask = _webSocketBridge.ShutdownAsync();
if (await Task.WhenAny(shutdownTask, Task.Delay(5_000)) != shutdownTask)
{
Trace.Warning("WebSocket DAP bridge shutdown timed out after 5s");
_ = shutdownTask.ContinueWith(
t => Trace.Error($"WebSocket DAP bridge shutdown faulted: {t.Exception?.GetBaseException().Message}"),
TaskContinuationOptions.OnlyOnFaulted);
}
else
{
Trace.Info("WebSocket DAP bridge stopped");
}

_webSocketBridge = null;
}

CleanupConnection();

// Cancel the connection loop first so AcceptTcpClientAsync unblocks
Expand Down Expand Up @@ -315,6 +352,7 @@ public async Task StopAsync()
_connectionLoopTask = null;
_loopCts?.Dispose();
_loopCts = null;
_webSocketBridge = null;
}

public async Task OnStepStartingAsync(IStep step)
Expand Down
Loading
Loading