Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8b1b23b
Get EnableDebugger from job context
rentziass Mar 10, 2026
cca15de
Add DAP protocol message types and service interfaces
rentziass Mar 11, 2026
9737dfa
Add DAP TCP server with reconnection support
rentziass Mar 11, 2026
17b05dd
Add minimal DAP debug session with next/continue support
rentziass Mar 11, 2026
915e13c
Integrate DAP debugger into JobRunner and StepsRunner
rentziass Mar 11, 2026
3d8c844
Add DapVariableProvider for scope inspection with centralized masking
rentziass Mar 12, 2026
2c65db1
Wire DapVariableProvider into DapDebugSession for scope inspection
rentziass Mar 12, 2026
0d33fd1
Add L0 tests for DAP scope inspection and secret masking
rentziass Mar 12, 2026
1573e36
Add expression evaluation to DapVariableProvider
rentziass Mar 12, 2026
f31e1c7
Wire evaluate request into DapDebugSession
rentziass Mar 12, 2026
2a98a8c
Add L0 tests for DAP expression evaluation
rentziass Mar 12, 2026
852e872
Add DAP REPL command model and parser
rentziass Mar 12, 2026
735dd69
Add DapReplExecutor for run command execution
rentziass Mar 12, 2026
165fb90
Wire REPL routing into DapDebugSession
rentziass Mar 12, 2026
b76917a
Add L0 tests for REPL parser and session routing
rentziass Mar 12, 2026
860a919
Fix expression expansion in REPL run command
rentziass Mar 12, 2026
8d6b38a
Add completions support and friendly errors for unsupported commands
rentziass Mar 12, 2026
a8f3b91
Harden DAP server
rentziass Mar 12, 2026
e4406e0
Fix debug session race conditions and step-flow bugs
rentziass Mar 12, 2026
75760d1
Centralize outbound DAP masking and harden secrets scope
rentziass Mar 12, 2026
649dc74
More tests
rentziass Mar 13, 2026
8d1e06f
Remove centralized masking
rentziass Mar 13, 2026
5bad8cb
Mask step display names
rentziass Mar 13, 2026
00bde90
remove waits
rentziass Mar 13, 2026
e11d6cf
lock state
rentziass Mar 13, 2026
7d0f26a
encoding casting
rentziass Mar 13, 2026
9d33c82
volatile state
rentziass Mar 13, 2026
9cd74b0
ci
rentziass Mar 13, 2026
7f39d40
Update src/Runner.Worker/JobRunner.cs
rentziass Mar 16, 2026
4bf2b29
Add DapDebugger facade
rentziass Mar 16, 2026
b36d9a6
PR feedback
rentziass Mar 16, 2026
437e20d
Fail the job if no connection in 15 minutes
rentziass Mar 16, 2026
f9919b2
PR Feedback
rentziass Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
905 changes: 905 additions & 0 deletions src/Runner.Worker/Dap/DapDebugSession.cs

Large diffs are not rendered by default.

186 changes: 186 additions & 0 deletions src/Runner.Worker/Dap/DapDebugger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Common;

namespace GitHub.Runner.Worker.Dap
{
/// <summary>
/// Single public facade for the Debug Adapter Protocol subsystem.
/// Owns the DapServer and DapDebugSession internally; external callers
/// (JobRunner, StepsRunner) interact only with this class.
/// </summary>
public sealed class DapDebugger : RunnerService, IDapDebugger
{
private const int DefaultPort = 4711;
private const int DefaultTimeoutMinutes = 15;
private const string PortEnvironmentVariable = "ACTIONS_RUNNER_DAP_PORT";
private const string TimeoutEnvironmentVariable = "ACTIONS_RUNNER_DAP_CONNECTION_TIMEOUT";

private IDapServer _server;
private IDapDebugSession _session;
private CancellationTokenRegistration? _cancellationRegistration;
private volatile bool _started;

public bool IsActive => _session?.IsActive == true;

public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
Trace.Info("DapDebugger initialized");
}

public async Task StartAsync(CancellationToken cancellationToken)
{
var port = ResolvePort();

_server = HostContext.GetService<IDapServer>();
_session = HostContext.GetService<IDapDebugSession>();

_server.SetSession(_session);
_session.SetDapServer(_server);

await _server.StartAsync(port, cancellationToken);
_started = true;

Trace.Info($"DAP debugger started on port {port}");
}

public async Task WaitUntilReadyAsync(CancellationToken cancellationToken)
{
if (!_started || _server == null || _session == null)
{
return;
}

var timeoutMinutes = ResolveTimeout();
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(timeoutMinutes));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);

try
{
Trace.Info($"Waiting for debugger client connection (timeout: {timeoutMinutes} minutes)...");
await _server.WaitForConnectionAsync(linkedCts.Token);
Trace.Info("Debugger client connected.");

await _session.WaitForHandshakeAsync(linkedCts.Token);
Trace.Info("DAP handshake complete.");
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
throw new TimeoutException($"No debugger client connected within {timeoutMinutes} minutes.");
}

_cancellationRegistration = cancellationToken.Register(() =>
{
Trace.Info("Job cancellation requested, cancelling debug session.");
_session.CancelSession();
});
}

public async Task StopAsync()
{
if (_cancellationRegistration.HasValue)
{
_cancellationRegistration.Value.Dispose();
_cancellationRegistration = null;
}

if (_server != null && _started)
{
try
{
Trace.Info("Stopping DAP debugger");
await _server.StopAsync();
}
catch (Exception ex)
{
Trace.Error("Error stopping DAP debugger");
Trace.Error(ex);
}
}

_started = false;
}

public void CancelSession()
{
_session?.CancelSession();
}

public async Task OnStepStartingAsync(IStep step, IExecutionContext jobContext, bool isFirstStep, CancellationToken cancellationToken)
{
if (!IsActive)
{
return;
}

try
{
await _session.OnStepStartingAsync(step, jobContext, isFirstStep, cancellationToken);
}
catch (Exception ex)
{
Trace.Warning($"DAP OnStepStarting error: {ex.Message}");
}
}

public void OnStepCompleted(IStep step)
{
if (!IsActive)
{
return;
}

try
{
_session.OnStepCompleted(step);
}
catch (Exception ex)
{
Trace.Warning($"DAP OnStepCompleted error: {ex.Message}");
}
}

public void OnJobCompleted()
{
if (!IsActive)
{
return;
}

try
{
_session.OnJobCompleted();
}
catch (Exception ex)
{
Trace.Warning($"DAP OnJobCompleted error: {ex.Message}");
}
}

private int ResolvePort()
{
var portEnv = Environment.GetEnvironmentVariable(PortEnvironmentVariable);
if (!string.IsNullOrEmpty(portEnv) && int.TryParse(portEnv, out var customPort) && customPort > 0 && customPort <= 65535)
{
Trace.Info($"Using custom DAP port {customPort} from {PortEnvironmentVariable}");
return customPort;
}

return DefaultPort;
}

private int ResolveTimeout()
{
var timeoutEnv = Environment.GetEnvironmentVariable(TimeoutEnvironmentVariable);
if (!string.IsNullOrEmpty(timeoutEnv) && int.TryParse(timeoutEnv, out var customTimeout) && customTimeout > 0)
{
Trace.Info($"Using custom DAP timeout {customTimeout} minutes from {TimeoutEnvironmentVariable}");
return customTimeout;
}

return DefaultTimeoutMinutes;
}
}
}
Loading
Loading