mirror of
https://github.com/actions/runner.git
synced 2026-07-03 11:06:08 +08:00
Add DAP job execution view source
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,7 @@ using Microsoft.DevTunnels.Connections;
|
||||
using Microsoft.DevTunnels.Contracts;
|
||||
using Microsoft.DevTunnels.Management;
|
||||
using Newtonsoft.Json;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
@@ -27,6 +28,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
public string DisplayName { get; set; }
|
||||
public TaskResult? Result { get; set; }
|
||||
public int FrameId { get; set; }
|
||||
public int? SourceLine { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -54,6 +56,9 @@ namespace GitHub.Runner.Worker.Dap
|
||||
// Frame IDs for completed steps start at 1000
|
||||
private const int _completedFrameIdBase = 1000;
|
||||
|
||||
// Stable session-scoped source reference for the synthesized job step list.
|
||||
private const int _jobStepsSourceReference = 1;
|
||||
|
||||
private TcpListener _listener;
|
||||
private TcpClient _client;
|
||||
private NetworkStream _stream;
|
||||
@@ -98,6 +103,8 @@ namespace GitHub.Runner.Worker.Dap
|
||||
// Track completed steps for stack trace
|
||||
private readonly List<CompletedStepInfo> _completedSteps = new List<CompletedStepInfo>();
|
||||
private int _nextCompletedFrameId = _completedFrameIdBase;
|
||||
private JobExecutionView _jobStepsSource;
|
||||
private bool _jobCompleted;
|
||||
|
||||
// Client connection tracking for reconnection support
|
||||
private volatile bool _isClientConnected;
|
||||
@@ -240,6 +247,179 @@ namespace GitHub.Runner.Worker.Dap
|
||||
}
|
||||
}
|
||||
|
||||
public Task OnJobStepsInitializedAsync(IEnumerable<IStep> steps, IEnumerable<IStep> initialPostSteps)
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IExecutionContext jobContext;
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state != DapSessionState.Ready &&
|
||||
_state != DapSessionState.Paused &&
|
||||
_state != DapSessionState.Running)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
jobContext = _jobContext;
|
||||
}
|
||||
|
||||
var stepList = steps?.Where(step => step != null).ToList() ?? new List<IStep>();
|
||||
var initialPostStepList = initialPostSteps?.Where(step => step != null).ToList() ?? new List<IStep>();
|
||||
var jobId = jobContext?.GetGitHubContext("job");
|
||||
var snapshot = new JobExecutionView(
|
||||
jobId,
|
||||
stepList,
|
||||
initialPostStepList,
|
||||
PredictPostSteps(jobContext, stepList, initialPostStepList));
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
_jobStepsSource = snapshot;
|
||||
_jobCompleted = false;
|
||||
}
|
||||
Trace.Info("DAP job steps source initialized");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning("DAP OnJobStepsInitialized error.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void OnPostStepRegistered(IStep step)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (step is IActionRunner postRunner && postRunner.Action != null)
|
||||
{
|
||||
JobExecutionView snapshot;
|
||||
lock (_stateLock)
|
||||
{
|
||||
snapshot = _jobStepsSource;
|
||||
}
|
||||
|
||||
var line = snapshot?.TryClaimPredictedStep(MatchKeyFor(postRunner.Action.Id), step);
|
||||
if (line.HasValue)
|
||||
{
|
||||
Trace.Info($"DAP job steps source claimed predicted post step '{step.DisplayName}' at line {line.Value}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Info($"DAP job steps source had no predicted line for post step '{step.DisplayName}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning("DAP OnPostStepRegistered error.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<JobExecutionView.PredictedPostStep> PredictPostSteps(
|
||||
IExecutionContext jobContext,
|
||||
IReadOnlyList<IStep> steps,
|
||||
IReadOnlyList<IStep> initialPostSteps)
|
||||
{
|
||||
if (jobContext == null || steps == null || steps.Count == 0)
|
||||
{
|
||||
return Array.Empty<JobExecutionView.PredictedPostStep>();
|
||||
}
|
||||
|
||||
IActionManager actionManager;
|
||||
try
|
||||
{
|
||||
actionManager = HostContext.GetService<IActionManager>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Info($"DAP post-step predictor skipped because IActionManager is unavailable ({ex.Message}).");
|
||||
return Array.Empty<JobExecutionView.PredictedPostStep>();
|
||||
}
|
||||
|
||||
var predictions = new List<JobExecutionView.PredictedPostStep>();
|
||||
var seenActionIds = new HashSet<Guid>();
|
||||
if (initialPostSteps != null)
|
||||
{
|
||||
foreach (var postStep in initialPostSteps)
|
||||
{
|
||||
if (postStep is IActionRunner postRunner && postRunner.Action != null)
|
||||
{
|
||||
seenActionIds.Add(postRunner.Action.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var step in steps)
|
||||
{
|
||||
if (step is not IActionRunner runner ||
|
||||
runner.Stage == ActionRunStage.Post ||
|
||||
runner.Action == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var action = runner.Action;
|
||||
if (action.Reference is not Pipelines.RepositoryPathReference repoRef)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!seenActionIds.Add(action.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Definition definition;
|
||||
try
|
||||
{
|
||||
definition = actionManager.LoadAction(jobContext, action);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Info($"DAP post-step predictor could not load action '{repoRef.Name}' ({ex.Message}).");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (definition?.Data?.Execution?.HasPost != true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
predictions.Add(new JobExecutionView.PredictedPostStep(
|
||||
GetPostDisplayName(runner),
|
||||
MatchKeyFor(action.Id)));
|
||||
}
|
||||
|
||||
predictions.Reverse();
|
||||
return predictions;
|
||||
}
|
||||
|
||||
private static string GetPostDisplayName(IActionRunner runner)
|
||||
{
|
||||
var displayName = string.IsNullOrEmpty(runner.DisplayName) ? "step" : runner.DisplayName;
|
||||
if (runner.Stage == ActionRunStage.Pre &&
|
||||
displayName.StartsWith("Pre ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
displayName = displayName.Substring("Pre ".Length);
|
||||
}
|
||||
|
||||
return $"Post {displayName}";
|
||||
}
|
||||
|
||||
private static string MatchKeyFor(Guid actionId)
|
||||
{
|
||||
return $"post:{actionId:N}";
|
||||
}
|
||||
|
||||
public async Task OnJobCompletedAsync()
|
||||
{
|
||||
if (_state != DapSessionState.NotStarted)
|
||||
@@ -253,6 +433,11 @@ namespace GitHub.Runner.Worker.Dap
|
||||
if (_jobContext != null)
|
||||
{
|
||||
Trace.Info("Job completed — pausing for inspection");
|
||||
lock (_stateLock)
|
||||
{
|
||||
_jobCompleted = true;
|
||||
}
|
||||
|
||||
SendStoppedEvent("completed", "Job completed — inspect variables before the session ends.");
|
||||
|
||||
await WaitForCommandAsync(_jobContext.CancellationToken);
|
||||
@@ -359,6 +544,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
_state = DapSessionState.Terminated;
|
||||
}
|
||||
_jobStepsSource = null;
|
||||
}
|
||||
|
||||
_isClientConnected = false;
|
||||
@@ -417,7 +603,8 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
DisplayName = step.DisplayName,
|
||||
Result = result,
|
||||
FrameId = _nextCompletedFrameId++
|
||||
FrameId = _nextCompletedFrameId++,
|
||||
SourceLine = _jobStepsSource?.TryGetLineForStep(step)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -468,6 +655,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
"next" => HandleNext(request),
|
||||
"setBreakpoints" => HandleSetBreakpoints(request),
|
||||
"setExceptionBreakpoints" => HandleSetExceptionBreakpoints(request),
|
||||
"source" => HandleSource(request),
|
||||
"completions" => HandleCompletions(request),
|
||||
"stepIn" => CreateResponse(request, false, "Step In is not supported. Actions jobs debug at the step level - use 'next' to advance to the next step.", body: null),
|
||||
"stepOut" => CreateResponse(request, false, "Step Out is not supported. Actions jobs debug at the step level - use 'continue' to resume.", body: null),
|
||||
@@ -857,6 +1045,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
bool pauseOnNextStep;
|
||||
CancellationToken cancellationToken;
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state != DapSessionState.Ready &&
|
||||
@@ -868,6 +1057,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
|
||||
_currentStep = step;
|
||||
_currentStepIndex = _completedSteps.Count;
|
||||
_jobCompleted = false;
|
||||
pauseOnNextStep = _pauseOnNextStep;
|
||||
cancellationToken = _jobContext?.CancellationToken ?? CancellationToken.None;
|
||||
}
|
||||
@@ -1050,29 +1240,46 @@ namespace GitHub.Runner.Worker.Dap
|
||||
private Response HandleStackTrace(Request request)
|
||||
{
|
||||
IStep currentStep;
|
||||
int currentStepIndex;
|
||||
CompletedStepInfo[] completedSteps;
|
||||
JobExecutionView jobStepsSource;
|
||||
bool jobCompleted;
|
||||
lock (_stateLock)
|
||||
{
|
||||
currentStep = _currentStep;
|
||||
currentStepIndex = _currentStepIndex;
|
||||
completedSteps = _completedSteps.ToArray();
|
||||
jobStepsSource = _jobStepsSource;
|
||||
jobCompleted = _jobCompleted;
|
||||
}
|
||||
|
||||
var frames = new List<StackFrame>();
|
||||
var source = jobStepsSource != null ? BuildJobStepsSource(jobStepsSource) : null;
|
||||
|
||||
// Add current step as the top frame
|
||||
if (currentStep != null)
|
||||
if (jobCompleted && jobStepsSource != null)
|
||||
{
|
||||
frames.Add(new StackFrame
|
||||
{
|
||||
Id = _currentFrameId,
|
||||
Name = "Complete job [completed]",
|
||||
Source = source,
|
||||
Line = jobStepsSource.CompleteJobLine,
|
||||
Column = 1,
|
||||
PresentationHint = "normal"
|
||||
});
|
||||
}
|
||||
else if (currentStep != null)
|
||||
{
|
||||
var resultIndicator = currentStep.ExecutionContext?.Result != null
|
||||
? $" [{currentStep.ExecutionContext.Result}]"
|
||||
: " [running]";
|
||||
var currentSourceLine = jobStepsSource?.TryGetLineForStep(currentStep);
|
||||
|
||||
frames.Add(new StackFrame
|
||||
{
|
||||
Id = _currentFrameId,
|
||||
Name = MaskUserVisibleText($"{currentStep.DisplayName ?? "Current Step"}{resultIndicator}"),
|
||||
Line = currentStepIndex + 1,
|
||||
Source = currentSourceLine.HasValue ? source : null,
|
||||
Line = currentSourceLine ?? 0,
|
||||
Column = 1,
|
||||
PresentationHint = "normal"
|
||||
});
|
||||
@@ -1098,7 +1305,8 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
Id = completedStep.FrameId,
|
||||
Name = MaskUserVisibleText($"{completedStep.DisplayName}{resultStr}"),
|
||||
Line = 1,
|
||||
Source = completedStep.SourceLine.HasValue ? source : null,
|
||||
Line = completedStep.SourceLine ?? 0,
|
||||
Column = 1,
|
||||
PresentationHint = "subtle"
|
||||
});
|
||||
@@ -1113,6 +1321,76 @@ namespace GitHub.Runner.Worker.Dap
|
||||
return CreateResponse(request, true, body: body);
|
||||
}
|
||||
|
||||
private Source BuildJobStepsSource(JobExecutionView snapshot)
|
||||
{
|
||||
return new Source
|
||||
{
|
||||
Name = MaskUserVisibleText(snapshot.SourceFileName),
|
||||
Path = MaskUserVisibleText($"{SanitizeSourcePathSegment(snapshot.JobId)}/{snapshot.SourceFileName}"),
|
||||
SourceReference = _jobStepsSourceReference,
|
||||
PresentationHint = "normal"
|
||||
};
|
||||
}
|
||||
|
||||
private static string SanitizeSourcePathSegment(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "job";
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(value.Length);
|
||||
foreach (var character in value)
|
||||
{
|
||||
builder.Append(char.IsControl(character) || character == '/' || character == '\\'
|
||||
? '_'
|
||||
: character);
|
||||
}
|
||||
|
||||
return builder.Length == 0 ? "job" : builder.ToString();
|
||||
}
|
||||
|
||||
internal Response HandleSource(Request request)
|
||||
{
|
||||
SourceArguments args;
|
||||
try
|
||||
{
|
||||
args = request.Arguments?.ToObject<SourceArguments>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"Failed to parse source arguments: {ex.GetType().Name}");
|
||||
return CreateResponse(request, false, "Invalid source arguments.", body: null);
|
||||
}
|
||||
|
||||
var sourceReference = args?.Source?.SourceReference ?? args?.SourceReference;
|
||||
if (!sourceReference.HasValue)
|
||||
{
|
||||
return CreateResponse(request, false, "Missing source reference.", body: null);
|
||||
}
|
||||
|
||||
JobExecutionView snapshot;
|
||||
lock (_stateLock)
|
||||
{
|
||||
snapshot = _jobStepsSource;
|
||||
}
|
||||
|
||||
if (snapshot == null)
|
||||
{
|
||||
return CreateResponse(request, false, "Job steps source not yet available.", body: null);
|
||||
}
|
||||
|
||||
if (sourceReference.Value != _jobStepsSourceReference)
|
||||
{
|
||||
return CreateResponse(request, false, $"Unknown source reference: {sourceReference.Value}.", body: null);
|
||||
}
|
||||
|
||||
return CreateResponse(request, true, body: new SourceResponseBody
|
||||
{
|
||||
Content = MaskUserVisibleText(snapshot.Content)
|
||||
});
|
||||
}
|
||||
|
||||
private Response HandleScopes(Request request)
|
||||
{
|
||||
var args = request.Arguments?.ToObject<ScopesArguments>();
|
||||
|
||||
@@ -537,6 +537,46 @@ namespace GitHub.Runner.Worker.Dap
|
||||
|
||||
#endregion
|
||||
|
||||
#region Source Request/Response
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for 'source' request.
|
||||
/// </summary>
|
||||
public class SourceArguments
|
||||
{
|
||||
/// <summary>
|
||||
/// Source descriptor. Some clients send sourceReference only here.
|
||||
/// </summary>
|
||||
[JsonProperty("source", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Source Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The reference to the source.
|
||||
/// </summary>
|
||||
[JsonProperty("sourceReference", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public int? SourceReference { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response body for 'source' request.
|
||||
/// </summary>
|
||||
public class SourceResponseBody
|
||||
{
|
||||
/// <summary>
|
||||
/// Content of the source as a string.
|
||||
/// </summary>
|
||||
[JsonProperty("content")]
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional content type / mime type of the source.
|
||||
/// </summary>
|
||||
[JsonProperty("mimeType", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string MimeType { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scopes Request/Response
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap
|
||||
@@ -19,6 +20,8 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
Task StartAsync(IExecutionContext jobContext);
|
||||
Task WaitUntilReadyAsync();
|
||||
Task OnJobStepsInitializedAsync(IEnumerable<IStep> steps, IEnumerable<IStep> initialPostSteps);
|
||||
void OnPostStepRegistered(IStep step);
|
||||
Task OnStepStartingAsync(IStep step);
|
||||
void OnStepCompleted(IStep step);
|
||||
Task OnJobCompletedAsync();
|
||||
|
||||
358
src/Runner.Worker/Dap/JobExecutionView.cs
Normal file
358
src/Runner.Worker/Dap/JobExecutionView.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
internal sealed class JobExecutionView
|
||||
{
|
||||
private const string _sourceFileName = "execution.yml";
|
||||
|
||||
private readonly object _lock = new object();
|
||||
private readonly List<SourceEntry> _preEntries = new List<SourceEntry>();
|
||||
private readonly List<SourceEntry> _mainEntries = new List<SourceEntry>();
|
||||
private readonly List<SourceEntry> _postEntries = new List<SourceEntry>();
|
||||
private readonly List<StepLine> _lineByStep = new List<StepLine>();
|
||||
private string _content;
|
||||
private int _completeJobLine;
|
||||
|
||||
public JobExecutionView(
|
||||
string jobId,
|
||||
IEnumerable<IStep> steps,
|
||||
IEnumerable<IStep> initialPostSteps,
|
||||
IEnumerable<PredictedPostStep> predictedPostSteps = null)
|
||||
{
|
||||
JobId = string.IsNullOrWhiteSpace(jobId) ? "job" : jobId;
|
||||
|
||||
_preEntries.Add(new SourceEntry("Setup job"));
|
||||
AddSteps(steps);
|
||||
AddPredictedPostSteps(predictedPostSteps);
|
||||
AddSteps(initialPostSteps);
|
||||
_postEntries.Add(SourceEntry.CreateSyntheticCompleteJob());
|
||||
Render();
|
||||
}
|
||||
|
||||
public string JobId { get; }
|
||||
public string SourceFileName => _sourceFileName;
|
||||
|
||||
public string Content
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int CompleteJobLine
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _completeJobLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int? TryClaimPredictedStep(string matchKey, IStep step)
|
||||
{
|
||||
if (string.IsNullOrEmpty(matchKey) || step == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var existingLine = TryGetLineForStepNoLock(step);
|
||||
if (existingLine.HasValue)
|
||||
{
|
||||
return existingLine;
|
||||
}
|
||||
|
||||
foreach (var entry in _postEntries)
|
||||
{
|
||||
if (!string.Equals(entry.MatchKey, matchKey, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.Step != null && !ReferenceEquals(entry.Step, step))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
entry.Step = step;
|
||||
Render();
|
||||
return TryGetLineForStepNoLock(step);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int? TryGetLineForStep(IStep step)
|
||||
{
|
||||
if (step == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
return TryGetLineForStepNoLock(step);
|
||||
}
|
||||
}
|
||||
|
||||
private int? TryGetLineForStepNoLock(IStep step)
|
||||
{
|
||||
foreach (var stepLine in _lineByStep)
|
||||
{
|
||||
if (ReferenceEquals(stepLine.Step, step))
|
||||
{
|
||||
return stepLine.Line;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void AddSteps(IEnumerable<IStep> steps)
|
||||
{
|
||||
if (steps == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var step in steps)
|
||||
{
|
||||
if (step == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
GetEntries(GetSection(step)).Add(new SourceEntry(step));
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPredictedPostSteps(IEnumerable<PredictedPostStep> steps)
|
||||
{
|
||||
if (steps == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var step in steps)
|
||||
{
|
||||
if (step == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_postEntries.Add(new SourceEntry(step.DisplayName, step.MatchKey));
|
||||
}
|
||||
}
|
||||
|
||||
private List<SourceEntry> GetEntries(SourceSection section)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
case SourceSection.Pre:
|
||||
return _preEntries;
|
||||
case SourceSection.Post:
|
||||
return _postEntries;
|
||||
default:
|
||||
return _mainEntries;
|
||||
}
|
||||
}
|
||||
|
||||
private static SourceSection GetSection(IStep step)
|
||||
{
|
||||
if (step is IActionRunner actionRunner)
|
||||
{
|
||||
return GetSection(actionRunner.Stage);
|
||||
}
|
||||
|
||||
if (step.ExecutionContext != null)
|
||||
{
|
||||
return GetSection(step.ExecutionContext.Stage);
|
||||
}
|
||||
|
||||
return SourceSection.Main;
|
||||
}
|
||||
|
||||
private static SourceSection GetSection(ActionRunStage stage)
|
||||
{
|
||||
switch (stage)
|
||||
{
|
||||
case ActionRunStage.Pre:
|
||||
return SourceSection.Pre;
|
||||
case ActionRunStage.Post:
|
||||
return SourceSection.Post;
|
||||
default:
|
||||
return SourceSection.Main;
|
||||
}
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
_lineByStep.Clear();
|
||||
_completeJobLine = 0;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var line = 1;
|
||||
|
||||
AppendSection(sb, "pre", _preEntries, ref line, appendSeparatorLine: true);
|
||||
AppendSection(sb, "main", _mainEntries, ref line, appendSeparatorLine: true);
|
||||
AppendSection(sb, "post", _postEntries, ref line, appendSeparatorLine: false);
|
||||
|
||||
_content = sb.ToString();
|
||||
}
|
||||
|
||||
private void AppendSection(
|
||||
StringBuilder sb,
|
||||
string sectionName,
|
||||
IReadOnlyList<SourceEntry> entries,
|
||||
ref int line,
|
||||
bool appendSeparatorLine)
|
||||
{
|
||||
sb.Append(sectionName).Append(":\n");
|
||||
line++;
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.Step != null && TryGetLineForStepNoLock(entry.Step) == null)
|
||||
{
|
||||
_lineByStep.Add(new StepLine(entry.Step, line));
|
||||
}
|
||||
|
||||
sb.Append(" - step: ");
|
||||
sb.Append(FormatYamlString(entry.DisplayName));
|
||||
sb.Append('\n');
|
||||
if (entry.IsSyntheticCompleteJob)
|
||||
{
|
||||
_completeJobLine = line;
|
||||
}
|
||||
|
||||
line++;
|
||||
}
|
||||
|
||||
if (appendSeparatorLine)
|
||||
{
|
||||
sb.Append('\n');
|
||||
line++;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatYamlString(string value)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append('"');
|
||||
foreach (var c in value)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '\\':
|
||||
sb.Append(@"\\");
|
||||
break;
|
||||
case '"':
|
||||
sb.Append("\\\"");
|
||||
break;
|
||||
case '\r':
|
||||
sb.Append(@"\r");
|
||||
break;
|
||||
case '\n':
|
||||
sb.Append(@"\n");
|
||||
break;
|
||||
case '\t':
|
||||
sb.Append(@"\t");
|
||||
break;
|
||||
default:
|
||||
if (char.IsControl(c))
|
||||
{
|
||||
sb.Append(@"\u");
|
||||
sb.Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append('"');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
internal sealed class PredictedPostStep
|
||||
{
|
||||
public PredictedPostStep(string displayName, string matchKey)
|
||||
{
|
||||
DisplayName = string.IsNullOrEmpty(displayName) ? "step" : displayName;
|
||||
MatchKey = matchKey;
|
||||
}
|
||||
|
||||
public string DisplayName { get; }
|
||||
public string MatchKey { get; }
|
||||
}
|
||||
|
||||
private sealed class StepLine
|
||||
{
|
||||
public StepLine(IStep step, int line)
|
||||
{
|
||||
Step = step;
|
||||
Line = line;
|
||||
}
|
||||
|
||||
public IStep Step { get; }
|
||||
public int Line { get; }
|
||||
}
|
||||
|
||||
private sealed class SourceEntry
|
||||
{
|
||||
public SourceEntry(string displayName)
|
||||
{
|
||||
DisplayName = string.IsNullOrEmpty(displayName) ? "step" : displayName;
|
||||
}
|
||||
|
||||
public SourceEntry(string displayName, string matchKey)
|
||||
: this(displayName)
|
||||
{
|
||||
MatchKey = matchKey;
|
||||
}
|
||||
|
||||
public SourceEntry(IStep step)
|
||||
{
|
||||
Step = step;
|
||||
DisplayName = string.IsNullOrEmpty(step.DisplayName) ? "step" : step.DisplayName;
|
||||
}
|
||||
|
||||
private SourceEntry(string displayName, bool isSyntheticCompleteJob)
|
||||
: this(displayName)
|
||||
{
|
||||
IsSyntheticCompleteJob = isSyntheticCompleteJob;
|
||||
}
|
||||
|
||||
public static SourceEntry CreateSyntheticCompleteJob()
|
||||
{
|
||||
return new SourceEntry("Complete job", isSyntheticCompleteJob: true);
|
||||
}
|
||||
|
||||
public IStep Step { get; set; }
|
||||
public string DisplayName { get; }
|
||||
public string MatchKey { get; }
|
||||
public bool IsSyntheticCompleteJob { get; }
|
||||
}
|
||||
|
||||
private enum SourceSection
|
||||
{
|
||||
Pre,
|
||||
Main,
|
||||
Post
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,6 +343,19 @@ namespace GitHub.Runner.Worker
|
||||
step.ExecutionContext.StepTelemetry.Action = step.DisplayName.ToLowerInvariant().Replace(' ', '_');
|
||||
}
|
||||
Root.PostJobSteps.Push(step);
|
||||
|
||||
if (Root.Global.Debugger?.Enabled == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
HostContext.GetService<Dap.IDapDebugger>().OnPostStepRegistered(step);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning("Failed to notify DAP debugger about registered post job step.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IExecutionContext CreateChild(
|
||||
|
||||
@@ -13,6 +13,7 @@ using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Services.WebApi;
|
||||
using Sdk.RSWebApi.Contracts;
|
||||
@@ -230,6 +231,12 @@ namespace GitHub.Runner.Worker
|
||||
jobContext.JobSteps.Enqueue(step);
|
||||
}
|
||||
|
||||
if (jobContext.Global.Debugger?.Enabled == true)
|
||||
{
|
||||
var dapDebugger = HostContext.GetService<IDapDebugger>();
|
||||
await dapDebugger.OnJobStepsInitializedAsync(jobContext.JobSteps, jobContext.PostJobSteps);
|
||||
}
|
||||
|
||||
await stepsRunner.RunAsync(jobContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in New Issue
Block a user