Skip to content
40 changes: 38 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 IWebSocketDapBridge _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,19 @@ 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 = HostContext.CreateService<IWebSocketDapBridge>();
_webSocketBridge.Start(debuggerConfig.Tunnel.Port, InternalDapPort);
}

// 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 +290,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 +350,7 @@ public async Task StopAsync()
_connectionLoopTask = null;
_loopCts?.Dispose();
_loopCts = null;
_webSocketBridge = null;
}

public async Task OnStepStartingAsync(IStep step)
Expand Down
12 changes: 12 additions & 0 deletions src/Runner.Worker/Dap/IWebSocketDapBridge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Threading.Tasks;
using GitHub.Runner.Common;

namespace GitHub.Runner.Worker.Dap
{
[ServiceLocator(Default = typeof(WebSocketDapBridge))]
public interface IWebSocketDapBridge : IRunnerService
{
void Start(int listenPort, int targetPort);
Task ShutdownAsync();
}
}
Loading
Loading