mirror of
https://github.com/actions/runner.git
synced 2026-01-16 16:58:29 +08:00
step-backwards working!
This commit is contained in:
1116
.opencode/plans/dap-step-backwards.md
Normal file
1116
.opencode/plans/dap-step-backwards.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,17 @@ namespace GitHub.Runner.Worker.Dap
|
||||
/// <summary>
|
||||
/// Disconnect from the debug session.
|
||||
/// </summary>
|
||||
Disconnect
|
||||
Disconnect,
|
||||
|
||||
/// <summary>
|
||||
/// Step back to the previous checkpoint.
|
||||
/// </summary>
|
||||
StepBack,
|
||||
|
||||
/// <summary>
|
||||
/// Reverse continue to the first checkpoint.
|
||||
/// </summary>
|
||||
ReverseContinue
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -118,6 +128,16 @@ namespace GitHub.Runner.Worker.Dap
|
||||
/// </summary>
|
||||
DapSessionState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of checkpoints available for step-back.
|
||||
/// </summary>
|
||||
int CheckpointCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a checkpoint restore is pending.
|
||||
/// </summary>
|
||||
bool HasPendingRestore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the DAP server for sending events.
|
||||
/// </summary>
|
||||
@@ -151,6 +171,48 @@ namespace GitHub.Runner.Worker.Dap
|
||||
/// Notifies the session that the job has completed.
|
||||
/// </summary>
|
||||
void OnJobCompleted();
|
||||
|
||||
/// <summary>
|
||||
/// Stores step info for potential checkpoint creation.
|
||||
/// Called at the start of OnStepStartingAsync, before pausing.
|
||||
/// </summary>
|
||||
/// <param name="step">The step about to execute</param>
|
||||
/// <param name="jobContext">The job execution context</param>
|
||||
/// <param name="stepIndex">The zero-based index of the step</param>
|
||||
/// <param name="remainingSteps">Steps remaining in the queue after this step</param>
|
||||
void SetPendingStepInfo(IStep step, IExecutionContext jobContext, int stepIndex, List<IStep> remainingSteps);
|
||||
|
||||
/// <summary>
|
||||
/// Clears pending step info after step completes or is skipped.
|
||||
/// </summary>
|
||||
void ClearPendingStepInfo();
|
||||
|
||||
/// <summary>
|
||||
/// Checks and consumes the flag indicating a checkpoint should be created.
|
||||
/// Called by StepsRunner after WaitForCommandAsync returns.
|
||||
/// </summary>
|
||||
/// <returns>True if a checkpoint should be created</returns>
|
||||
bool ShouldCreateCheckpoint();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a checkpoint for the pending step, capturing current state.
|
||||
/// Called when user issues next/continue command, after any REPL modifications.
|
||||
/// </summary>
|
||||
/// <param name="jobContext">The job execution context</param>
|
||||
void CreateCheckpointForPendingStep(IExecutionContext jobContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets and clears the checkpoint that was just restored (for StepsRunner to use).
|
||||
/// </summary>
|
||||
/// <returns>The restored checkpoint, or null if none</returns>
|
||||
StepCheckpoint ConsumeRestoredCheckpoint();
|
||||
|
||||
/// <summary>
|
||||
/// Restores job state to a previous checkpoint.
|
||||
/// </summary>
|
||||
/// <param name="checkpointIndex">The index of the checkpoint to restore</param>
|
||||
/// <param name="jobContext">The job execution context</param>
|
||||
void RestoreCheckpoint(int checkpointIndex, IExecutionContext jobContext);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -190,10 +252,30 @@ namespace GitHub.Runner.Worker.Dap
|
||||
// Variable provider for converting contexts to DAP variables
|
||||
private DapVariableProvider _variableProvider;
|
||||
|
||||
// Checkpoint storage for step-back (time-travel) debugging
|
||||
private readonly List<StepCheckpoint> _checkpoints = new List<StepCheckpoint>();
|
||||
private const int MaxCheckpoints = 50;
|
||||
|
||||
// Track pending step info for checkpoint creation (set during OnStepStartingAsync)
|
||||
private IStep _pendingStep;
|
||||
private List<IStep> _pendingRemainingSteps;
|
||||
private int _pendingStepIndex;
|
||||
|
||||
// Flag to signal checkpoint creation when user continues
|
||||
private bool _shouldCreateCheckpoint = false;
|
||||
|
||||
// Signal pending restoration to StepsRunner
|
||||
private int? _pendingRestoreCheckpoint = null;
|
||||
private StepCheckpoint _restoredCheckpoint = null;
|
||||
|
||||
public bool IsActive => _state == DapSessionState.Ready || _state == DapSessionState.Paused || _state == DapSessionState.Running;
|
||||
|
||||
public DapSessionState State => _state;
|
||||
|
||||
public int CheckpointCount => _checkpoints.Count;
|
||||
|
||||
public bool HasPendingRestore => _pendingRestoreCheckpoint.HasValue;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
@@ -229,6 +311,8 @@ namespace GitHub.Runner.Worker.Dap
|
||||
"evaluate" => await HandleEvaluateAsync(request),
|
||||
"setBreakpoints" => HandleSetBreakpoints(request),
|
||||
"setExceptionBreakpoints" => HandleSetExceptionBreakpoints(request),
|
||||
"stepBack" => HandleStepBack(request),
|
||||
"reverseContinue" => HandleReverseContinue(request),
|
||||
_ => CreateErrorResponse($"Unknown command: {request.Command}")
|
||||
};
|
||||
}
|
||||
@@ -259,8 +343,9 @@ namespace GitHub.Runner.Worker.Dap
|
||||
SupportsEvaluateForHovers = true,
|
||||
SupportTerminateDebuggee = true,
|
||||
SupportsTerminateRequest = true,
|
||||
// Step back (time-travel) debugging is supported
|
||||
SupportsStepBack = true,
|
||||
// We don't support these features (yet)
|
||||
SupportsStepBack = false,
|
||||
SupportsSetVariable = false,
|
||||
SupportsRestartFrame = false,
|
||||
SupportsGotoTargetsRequest = false,
|
||||
@@ -453,6 +538,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
_state = DapSessionState.Running;
|
||||
_pauseOnNextStep = false; // Don't pause on next step
|
||||
_shouldCreateCheckpoint = true; // Signal to create checkpoint before step executes
|
||||
_commandTcs?.TrySetResult(DapCommand.Continue);
|
||||
}
|
||||
}
|
||||
@@ -473,6 +559,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
_state = DapSessionState.Running;
|
||||
_pauseOnNextStep = true; // Pause before next step
|
||||
_shouldCreateCheckpoint = true; // Signal to create checkpoint before step executes
|
||||
_commandTcs?.TrySetResult(DapCommand.Next);
|
||||
}
|
||||
}
|
||||
@@ -898,6 +985,60 @@ namespace GitHub.Runner.Worker.Dap
|
||||
});
|
||||
}
|
||||
|
||||
private Response HandleStepBack(Request request)
|
||||
{
|
||||
Trace.Info("StepBack command received");
|
||||
|
||||
if (_checkpoints.Count == 0)
|
||||
{
|
||||
return CreateErrorResponse("No checkpoints available. Cannot step back before any steps have executed.");
|
||||
}
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state != DapSessionState.Paused)
|
||||
{
|
||||
return CreateErrorResponse("Can only step back when paused");
|
||||
}
|
||||
|
||||
// Step back to the most recent checkpoint
|
||||
// (which represents the state before the last executed step)
|
||||
int targetCheckpoint = _checkpoints.Count - 1;
|
||||
_pendingRestoreCheckpoint = targetCheckpoint;
|
||||
_state = DapSessionState.Running;
|
||||
_pauseOnNextStep = true; // Pause immediately after restore
|
||||
_commandTcs?.TrySetResult(DapCommand.StepBack);
|
||||
}
|
||||
|
||||
return CreateSuccessResponse(null);
|
||||
}
|
||||
|
||||
private Response HandleReverseContinue(Request request)
|
||||
{
|
||||
Trace.Info("ReverseContinue command received");
|
||||
|
||||
if (_checkpoints.Count == 0)
|
||||
{
|
||||
return CreateErrorResponse("No checkpoints available. Cannot reverse continue before any steps have executed.");
|
||||
}
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state != DapSessionState.Paused)
|
||||
{
|
||||
return CreateErrorResponse("Can only reverse continue when paused");
|
||||
}
|
||||
|
||||
// Go back to the first checkpoint (beginning of job)
|
||||
_pendingRestoreCheckpoint = 0;
|
||||
_state = DapSessionState.Running;
|
||||
_pauseOnNextStep = true;
|
||||
_commandTcs?.TrySetResult(DapCommand.ReverseContinue);
|
||||
}
|
||||
|
||||
return CreateSuccessResponse(null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Step Coordination (called by StepsRunner)
|
||||
@@ -1058,6 +1199,314 @@ namespace GitHub.Runner.Worker.Dap
|
||||
|
||||
#endregion
|
||||
|
||||
#region Checkpoint Methods (Time-Travel Debugging)
|
||||
|
||||
/// <summary>
|
||||
/// Stores step info for potential checkpoint creation.
|
||||
/// Called at the start of step processing, before pausing.
|
||||
/// </summary>
|
||||
public void SetPendingStepInfo(IStep step, IExecutionContext jobContext, int stepIndex, List<IStep> remainingSteps)
|
||||
{
|
||||
_pendingStep = step;
|
||||
_pendingStepIndex = stepIndex;
|
||||
_pendingRemainingSteps = remainingSteps;
|
||||
Trace.Info($"Pending step info set: '{step.DisplayName}' (index {stepIndex}, {remainingSteps.Count} remaining)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears pending step info after step completes or is skipped.
|
||||
/// </summary>
|
||||
public void ClearPendingStepInfo()
|
||||
{
|
||||
_pendingStep = null;
|
||||
_pendingRemainingSteps = null;
|
||||
_pendingStepIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks and consumes the flag indicating a checkpoint should be created.
|
||||
/// Called by StepsRunner after WaitForCommandAsync returns.
|
||||
/// </summary>
|
||||
public bool ShouldCreateCheckpoint()
|
||||
{
|
||||
var should = _shouldCreateCheckpoint;
|
||||
_shouldCreateCheckpoint = false;
|
||||
return should;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when user issues next/continue command.
|
||||
/// Creates checkpoint capturing current state (including any REPL modifications).
|
||||
/// </summary>
|
||||
public void CreateCheckpointForPendingStep(IExecutionContext jobContext)
|
||||
{
|
||||
if (_pendingStep == null)
|
||||
{
|
||||
Trace.Warning("CreateCheckpointForPendingStep called but no pending step");
|
||||
return;
|
||||
}
|
||||
|
||||
// Enforce maximum checkpoint limit
|
||||
if (_checkpoints.Count >= MaxCheckpoints)
|
||||
{
|
||||
_checkpoints.RemoveAt(0); // Remove oldest
|
||||
Trace.Info($"Removed oldest checkpoint (exceeded max {MaxCheckpoints})");
|
||||
}
|
||||
|
||||
var checkpoint = new StepCheckpoint
|
||||
{
|
||||
StepIndex = _pendingStepIndex,
|
||||
StepDisplayName = _pendingStep.DisplayName,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CurrentStep = _pendingStep,
|
||||
RemainingSteps = new List<IStep>(_pendingRemainingSteps),
|
||||
|
||||
// Deep copy environment variables - captures any REPL modifications
|
||||
EnvironmentVariables = new Dictionary<string, string>(
|
||||
jobContext.Global.EnvironmentVariables,
|
||||
StringComparer.OrdinalIgnoreCase),
|
||||
|
||||
// Deep copy env context
|
||||
EnvContextData = CopyEnvContextData(jobContext),
|
||||
|
||||
// Copy prepend path
|
||||
PrependPath = new List<string>(jobContext.Global.PrependPath),
|
||||
|
||||
// Copy job state
|
||||
JobResult = jobContext.Result,
|
||||
JobStatus = jobContext.JobContext.Status,
|
||||
|
||||
// Snapshot steps context
|
||||
StepsSnapshot = SnapshotStepsContext(jobContext.Global.StepsContext, jobContext.ScopeName)
|
||||
};
|
||||
|
||||
_checkpoints.Add(checkpoint);
|
||||
Trace.Info($"Created checkpoint [{_checkpoints.Count - 1}] for step '{_pendingStep.DisplayName}' " +
|
||||
$"(env vars: {checkpoint.EnvironmentVariables.Count}, " +
|
||||
$"prepend paths: {checkpoint.PrependPath.Count})");
|
||||
|
||||
// Send notification to debugger
|
||||
_server?.SendEvent(new Event
|
||||
{
|
||||
EventType = "output",
|
||||
Body = new OutputEventBody
|
||||
{
|
||||
Category = "console",
|
||||
Output = $"Checkpoint [{_checkpoints.Count - 1}] created for step '{_pendingStep.DisplayName}'\n"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets and clears the checkpoint that should be restored (for StepsRunner to use).
|
||||
/// Returns the checkpoint from the pending restore index if set.
|
||||
/// The returned checkpoint's CheckpointIndex property is set for use with RestoreCheckpoint().
|
||||
/// </summary>
|
||||
public StepCheckpoint ConsumeRestoredCheckpoint()
|
||||
{
|
||||
// If there's a pending restore, get the checkpoint from the index
|
||||
// (This is the correct checkpoint to restore to)
|
||||
if (_pendingRestoreCheckpoint.HasValue)
|
||||
{
|
||||
var checkpointIndex = _pendingRestoreCheckpoint.Value;
|
||||
if (checkpointIndex >= 0 && checkpointIndex < _checkpoints.Count)
|
||||
{
|
||||
var checkpoint = _checkpoints[checkpointIndex];
|
||||
// Ensure the checkpoint knows its own index (for RestoreCheckpoint call)
|
||||
checkpoint.CheckpointIndex = checkpointIndex;
|
||||
// Clear the pending state - the caller will handle restoration
|
||||
_pendingRestoreCheckpoint = null;
|
||||
_restoredCheckpoint = null;
|
||||
return checkpoint;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to already-restored checkpoint (shouldn't normally be used)
|
||||
var restoredCheckpoint = _restoredCheckpoint;
|
||||
_restoredCheckpoint = null;
|
||||
_pendingRestoreCheckpoint = null;
|
||||
return restoredCheckpoint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores job state to a previous checkpoint.
|
||||
/// </summary>
|
||||
public void RestoreCheckpoint(int checkpointIndex, IExecutionContext jobContext)
|
||||
{
|
||||
if (checkpointIndex < 0 || checkpointIndex >= _checkpoints.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(checkpointIndex),
|
||||
$"Checkpoint index {checkpointIndex} out of range (0-{_checkpoints.Count - 1})");
|
||||
}
|
||||
|
||||
var checkpoint = _checkpoints[checkpointIndex];
|
||||
Trace.Info($"Restoring checkpoint [{checkpointIndex}] for step '{checkpoint.StepDisplayName}'");
|
||||
|
||||
// Restore environment variables
|
||||
jobContext.Global.EnvironmentVariables.Clear();
|
||||
foreach (var kvp in checkpoint.EnvironmentVariables)
|
||||
{
|
||||
jobContext.Global.EnvironmentVariables[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
// Restore env context
|
||||
RestoreEnvContext(jobContext, checkpoint.EnvContextData);
|
||||
|
||||
// Restore prepend path
|
||||
jobContext.Global.PrependPath.Clear();
|
||||
jobContext.Global.PrependPath.AddRange(checkpoint.PrependPath);
|
||||
|
||||
// Restore job result
|
||||
jobContext.Result = checkpoint.JobResult;
|
||||
jobContext.JobContext.Status = checkpoint.JobStatus;
|
||||
|
||||
// Note: StepsContext restoration is complex and requires internal access
|
||||
// For now, we just log this limitation
|
||||
Trace.Info($"Steps context restoration: {checkpoint.StepsSnapshot.Count} steps in snapshot (partial restoration)");
|
||||
|
||||
// Clear checkpoints after this one (they're now invalid)
|
||||
if (checkpointIndex + 1 < _checkpoints.Count)
|
||||
{
|
||||
var removeCount = _checkpoints.Count - checkpointIndex - 1;
|
||||
_checkpoints.RemoveRange(checkpointIndex + 1, removeCount);
|
||||
Trace.Info($"Removed {removeCount} invalidated checkpoints");
|
||||
}
|
||||
|
||||
// Clear completed steps list for frames after this checkpoint
|
||||
while (_completedSteps.Count > checkpointIndex)
|
||||
{
|
||||
_completedSteps.RemoveAt(_completedSteps.Count - 1);
|
||||
}
|
||||
|
||||
// Store restored checkpoint for StepsRunner to consume
|
||||
_restoredCheckpoint = checkpoint;
|
||||
|
||||
// Send notification to debugger
|
||||
_server?.SendEvent(new Event
|
||||
{
|
||||
EventType = "output",
|
||||
Body = new OutputEventBody
|
||||
{
|
||||
Category = "console",
|
||||
Output = $"Restored to checkpoint [{checkpointIndex}] before step '{checkpoint.StepDisplayName}'\n" +
|
||||
$"Note: Filesystem changes were NOT reverted\n"
|
||||
}
|
||||
});
|
||||
|
||||
Trace.Info($"Checkpoint restored. {_checkpoints.Count} checkpoints remain.");
|
||||
}
|
||||
|
||||
private Dictionary<string, string> CopyEnvContextData(IExecutionContext context)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (context.ExpressionValues.TryGetValue("env", out var envData))
|
||||
{
|
||||
if (envData is DictionaryContextData dict)
|
||||
{
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
if (kvp.Value is StringContextData strData)
|
||||
{
|
||||
result[kvp.Key] = strData.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (envData is CaseSensitiveDictionaryContextData csDict)
|
||||
{
|
||||
foreach (var kvp in csDict)
|
||||
{
|
||||
if (kvp.Value is StringContextData strData)
|
||||
{
|
||||
result[kvp.Key] = strData.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void RestoreEnvContext(IExecutionContext context, Dictionary<string, string> envData)
|
||||
{
|
||||
// Create a new env context with restored data and replace the old one
|
||||
// Since DictionaryContextData doesn't have a Clear method, we replace the entire object
|
||||
#if OS_WINDOWS
|
||||
var newEnvContext = new DictionaryContextData();
|
||||
#else
|
||||
var newEnvContext = new CaseSensitiveDictionaryContextData();
|
||||
#endif
|
||||
foreach (var kvp in envData)
|
||||
{
|
||||
newEnvContext[kvp.Key] = new StringContextData(kvp.Value);
|
||||
}
|
||||
|
||||
context.ExpressionValues["env"] = newEnvContext;
|
||||
}
|
||||
|
||||
private Dictionary<string, StepStateSnapshot> SnapshotStepsContext(StepsContext stepsContext, string scopeName)
|
||||
{
|
||||
var result = new Dictionary<string, StepStateSnapshot>();
|
||||
|
||||
// Get the scope's context data
|
||||
var scopeData = stepsContext.GetScope(scopeName);
|
||||
if (scopeData != null)
|
||||
{
|
||||
foreach (var stepEntry in scopeData)
|
||||
{
|
||||
if (stepEntry.Value is DictionaryContextData stepData)
|
||||
{
|
||||
var snapshot = new StepStateSnapshot
|
||||
{
|
||||
Outputs = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
// Extract outcome
|
||||
if (stepData.TryGetValue("outcome", out var outcome) && outcome is StringContextData outcomeStr)
|
||||
{
|
||||
snapshot.Outcome = ParseActionResult(outcomeStr.Value);
|
||||
}
|
||||
|
||||
// Extract conclusion
|
||||
if (stepData.TryGetValue("conclusion", out var conclusion) && conclusion is StringContextData conclusionStr)
|
||||
{
|
||||
snapshot.Conclusion = ParseActionResult(conclusionStr.Value);
|
||||
}
|
||||
|
||||
// Extract outputs
|
||||
if (stepData.TryGetValue("outputs", out var outputs) && outputs is DictionaryContextData outputsDict)
|
||||
{
|
||||
foreach (var output in outputsDict)
|
||||
{
|
||||
if (output.Value is StringContextData outputStr)
|
||||
{
|
||||
snapshot.Outputs[output.Key] = outputStr.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result[$"{scopeName}/{stepEntry.Key}"] = snapshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ActionResult? ParseActionResult(string value)
|
||||
{
|
||||
return value?.ToLower() switch
|
||||
{
|
||||
"success" => ActionResult.Success,
|
||||
"failure" => ActionResult.Failure,
|
||||
"cancelled" => ActionResult.Cancelled,
|
||||
"skipped" => ActionResult.Skipped,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Response Helpers
|
||||
|
||||
private Response CreateSuccessResponse(object body)
|
||||
|
||||
87
src/Runner.Worker/Dap/StepCheckpoint.cs
Normal file
87
src/Runner.Worker/Dap/StepCheckpoint.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a snapshot of job state captured just before a step executes.
|
||||
/// Created when user issues next/continue command, after any REPL modifications.
|
||||
/// Used for step-back (time-travel) debugging.
|
||||
/// </summary>
|
||||
public sealed class StepCheckpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Index of this checkpoint in the checkpoints list.
|
||||
/// Used when restoring to identify which checkpoint to restore to.
|
||||
/// </summary>
|
||||
public int CheckpointIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Zero-based index of the step in the job.
|
||||
/// </summary>
|
||||
public int StepIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Display name of the step this checkpoint was created for.
|
||||
/// </summary>
|
||||
public string StepDisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of Global.EnvironmentVariables.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> EnvironmentVariables { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of ExpressionValues["env"] context data.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> EnvContextData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of Global.PrependPath.
|
||||
/// </summary>
|
||||
public List<string> PrependPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of job result.
|
||||
/// </summary>
|
||||
public TaskResult? JobResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of job status.
|
||||
/// </summary>
|
||||
public ActionResult? JobStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of steps context (outputs, outcomes, conclusions).
|
||||
/// Key is "{scopeName}/{stepName}", value is the step's state.
|
||||
/// </summary>
|
||||
public Dictionary<string, StepStateSnapshot> StepsSnapshot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The step that was about to execute (for re-running).
|
||||
/// </summary>
|
||||
public IStep CurrentStep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Steps remaining in the queue after CurrentStep.
|
||||
/// </summary>
|
||||
public List<IStep> RemainingSteps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this checkpoint was created.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of a single step's state in the steps context.
|
||||
/// </summary>
|
||||
public sealed class StepStateSnapshot
|
||||
{
|
||||
public ActionResult? Outcome { get; set; }
|
||||
public ActionResult? Conclusion { get; set; }
|
||||
public Dictionary<string, string> Outputs { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
@@ -56,6 +57,7 @@ namespace GitHub.Runner.Worker
|
||||
// The session's IsActive property determines if debugging is actually enabled
|
||||
var debugSession = HostContext.GetService<IDapDebugSession>();
|
||||
bool isFirstStep = true;
|
||||
int stepIndex = 0; // Track step index for checkpoints
|
||||
|
||||
while (jobContext.JobSteps.Count > 0 || !checkPostJobActions)
|
||||
{
|
||||
@@ -72,6 +74,9 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
var step = jobContext.JobSteps.Dequeue();
|
||||
|
||||
// Capture remaining steps for potential checkpoint (before we modify the queue)
|
||||
var remainingSteps = jobContext.JobSteps.ToList();
|
||||
|
||||
Trace.Info($"Processing step: DisplayName='{step.DisplayName}'");
|
||||
ArgUtil.NotNull(step.ExecutionContext, nameof(step.ExecutionContext));
|
||||
ArgUtil.NotNull(step.ExecutionContext.Global, nameof(step.ExecutionContext.Global));
|
||||
@@ -192,8 +197,52 @@ namespace GitHub.Runner.Worker
|
||||
// This happens after expression values are set up so the debugger can inspect variables
|
||||
if (debugSession?.IsActive == true)
|
||||
{
|
||||
// Store step info for checkpoint creation later
|
||||
debugSession.SetPendingStepInfo(step, jobContext, stepIndex, remainingSteps);
|
||||
|
||||
// Pause and wait for user command (next/continue/stepBack/reverseContinue)
|
||||
await debugSession.OnStepStartingAsync(step, jobContext, isFirstStep);
|
||||
isFirstStep = false;
|
||||
|
||||
// Check if user requested to step back
|
||||
if (debugSession.HasPendingRestore)
|
||||
{
|
||||
var checkpoint = debugSession.ConsumeRestoredCheckpoint();
|
||||
if (checkpoint != null)
|
||||
{
|
||||
// Restore the checkpoint state using the correct checkpoint index
|
||||
debugSession.RestoreCheckpoint(checkpoint.CheckpointIndex, jobContext);
|
||||
|
||||
// Re-queue the steps from checkpoint
|
||||
while (jobContext.JobSteps.Count > 0)
|
||||
{
|
||||
jobContext.JobSteps.Dequeue();
|
||||
}
|
||||
|
||||
// Queue the checkpoint's step and remaining steps
|
||||
jobContext.JobSteps.Enqueue(checkpoint.CurrentStep);
|
||||
foreach (var remainingStep in checkpoint.RemainingSteps)
|
||||
{
|
||||
jobContext.JobSteps.Enqueue(remainingStep);
|
||||
}
|
||||
|
||||
// Reset step index to checkpoint's index
|
||||
stepIndex = checkpoint.StepIndex;
|
||||
|
||||
// Clear pending step info since we're not executing this step
|
||||
debugSession.ClearPendingStepInfo();
|
||||
|
||||
// Skip to next iteration - will process restored step
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// User pressed next/continue - create checkpoint NOW
|
||||
// This captures any REPL modifications made while paused
|
||||
if (debugSession.ShouldCreateCheckpoint())
|
||||
{
|
||||
debugSession.CreateCheckpointForPendingStep(jobContext);
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate condition
|
||||
@@ -253,6 +302,9 @@ namespace GitHub.Runner.Worker
|
||||
jobCancelRegister?.Dispose();
|
||||
jobCancelRegister = null;
|
||||
}
|
||||
|
||||
// Clear pending step info after step completes
|
||||
debugSession?.ClearPendingStepInfo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +326,9 @@ namespace GitHub.Runner.Worker
|
||||
debugSession.OnStepCompleted(step);
|
||||
}
|
||||
|
||||
// Increment step index for checkpoint tracking
|
||||
stepIndex++;
|
||||
|
||||
Trace.Info($"Current state: job state = '{jobContext.Result}'");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user