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>
|
/// <summary>
|
||||||
/// Disconnect from the debug session.
|
/// Disconnect from the debug session.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Disconnect
|
Disconnect,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Step back to the previous checkpoint.
|
||||||
|
/// </summary>
|
||||||
|
StepBack,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reverse continue to the first checkpoint.
|
||||||
|
/// </summary>
|
||||||
|
ReverseContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -118,6 +128,16 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
DapSessionState State { get; }
|
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>
|
/// <summary>
|
||||||
/// Sets the DAP server for sending events.
|
/// Sets the DAP server for sending events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -151,6 +171,48 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
/// Notifies the session that the job has completed.
|
/// Notifies the session that the job has completed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void OnJobCompleted();
|
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>
|
/// <summary>
|
||||||
@@ -190,10 +252,30 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
// Variable provider for converting contexts to DAP variables
|
// Variable provider for converting contexts to DAP variables
|
||||||
private DapVariableProvider _variableProvider;
|
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 bool IsActive => _state == DapSessionState.Ready || _state == DapSessionState.Paused || _state == DapSessionState.Running;
|
||||||
|
|
||||||
public DapSessionState State => _state;
|
public DapSessionState State => _state;
|
||||||
|
|
||||||
|
public int CheckpointCount => _checkpoints.Count;
|
||||||
|
|
||||||
|
public bool HasPendingRestore => _pendingRestoreCheckpoint.HasValue;
|
||||||
|
|
||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
{
|
{
|
||||||
base.Initialize(hostContext);
|
base.Initialize(hostContext);
|
||||||
@@ -229,6 +311,8 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
"evaluate" => await HandleEvaluateAsync(request),
|
"evaluate" => await HandleEvaluateAsync(request),
|
||||||
"setBreakpoints" => HandleSetBreakpoints(request),
|
"setBreakpoints" => HandleSetBreakpoints(request),
|
||||||
"setExceptionBreakpoints" => HandleSetExceptionBreakpoints(request),
|
"setExceptionBreakpoints" => HandleSetExceptionBreakpoints(request),
|
||||||
|
"stepBack" => HandleStepBack(request),
|
||||||
|
"reverseContinue" => HandleReverseContinue(request),
|
||||||
_ => CreateErrorResponse($"Unknown command: {request.Command}")
|
_ => CreateErrorResponse($"Unknown command: {request.Command}")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -259,8 +343,9 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
SupportsEvaluateForHovers = true,
|
SupportsEvaluateForHovers = true,
|
||||||
SupportTerminateDebuggee = true,
|
SupportTerminateDebuggee = true,
|
||||||
SupportsTerminateRequest = true,
|
SupportsTerminateRequest = true,
|
||||||
|
// Step back (time-travel) debugging is supported
|
||||||
|
SupportsStepBack = true,
|
||||||
// We don't support these features (yet)
|
// We don't support these features (yet)
|
||||||
SupportsStepBack = false,
|
|
||||||
SupportsSetVariable = false,
|
SupportsSetVariable = false,
|
||||||
SupportsRestartFrame = false,
|
SupportsRestartFrame = false,
|
||||||
SupportsGotoTargetsRequest = false,
|
SupportsGotoTargetsRequest = false,
|
||||||
@@ -453,6 +538,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
{
|
{
|
||||||
_state = DapSessionState.Running;
|
_state = DapSessionState.Running;
|
||||||
_pauseOnNextStep = false; // Don't pause on next step
|
_pauseOnNextStep = false; // Don't pause on next step
|
||||||
|
_shouldCreateCheckpoint = true; // Signal to create checkpoint before step executes
|
||||||
_commandTcs?.TrySetResult(DapCommand.Continue);
|
_commandTcs?.TrySetResult(DapCommand.Continue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -473,6 +559,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
{
|
{
|
||||||
_state = DapSessionState.Running;
|
_state = DapSessionState.Running;
|
||||||
_pauseOnNextStep = true; // Pause before next step
|
_pauseOnNextStep = true; // Pause before next step
|
||||||
|
_shouldCreateCheckpoint = true; // Signal to create checkpoint before step executes
|
||||||
_commandTcs?.TrySetResult(DapCommand.Next);
|
_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
|
#endregion
|
||||||
|
|
||||||
#region Step Coordination (called by StepsRunner)
|
#region Step Coordination (called by StepsRunner)
|
||||||
@@ -1058,6 +1199,314 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
|
|
||||||
#endregion
|
#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
|
#region Response Helpers
|
||||||
|
|
||||||
private Response CreateSuccessResponse(object body)
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.Expressions2;
|
using GitHub.DistributedTask.Expressions2;
|
||||||
@@ -56,6 +57,7 @@ namespace GitHub.Runner.Worker
|
|||||||
// The session's IsActive property determines if debugging is actually enabled
|
// The session's IsActive property determines if debugging is actually enabled
|
||||||
var debugSession = HostContext.GetService<IDapDebugSession>();
|
var debugSession = HostContext.GetService<IDapDebugSession>();
|
||||||
bool isFirstStep = true;
|
bool isFirstStep = true;
|
||||||
|
int stepIndex = 0; // Track step index for checkpoints
|
||||||
|
|
||||||
while (jobContext.JobSteps.Count > 0 || !checkPostJobActions)
|
while (jobContext.JobSteps.Count > 0 || !checkPostJobActions)
|
||||||
{
|
{
|
||||||
@@ -72,6 +74,9 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
var step = jobContext.JobSteps.Dequeue();
|
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}'");
|
Trace.Info($"Processing step: DisplayName='{step.DisplayName}'");
|
||||||
ArgUtil.NotNull(step.ExecutionContext, nameof(step.ExecutionContext));
|
ArgUtil.NotNull(step.ExecutionContext, nameof(step.ExecutionContext));
|
||||||
ArgUtil.NotNull(step.ExecutionContext.Global, nameof(step.ExecutionContext.Global));
|
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
|
// This happens after expression values are set up so the debugger can inspect variables
|
||||||
if (debugSession?.IsActive == true)
|
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);
|
await debugSession.OnStepStartingAsync(step, jobContext, isFirstStep);
|
||||||
isFirstStep = false;
|
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
|
// Evaluate condition
|
||||||
@@ -253,6 +302,9 @@ namespace GitHub.Runner.Worker
|
|||||||
jobCancelRegister?.Dispose();
|
jobCancelRegister?.Dispose();
|
||||||
jobCancelRegister = null;
|
jobCancelRegister = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear pending step info after step completes
|
||||||
|
debugSession?.ClearPendingStepInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +326,9 @@ namespace GitHub.Runner.Worker
|
|||||||
debugSession.OnStepCompleted(step);
|
debugSession.OnStepCompleted(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Increment step index for checkpoint tracking
|
||||||
|
stepIndex++;
|
||||||
|
|
||||||
Trace.Info($"Current state: job state = '{jobContext.Result}'");
|
Trace.Info($"Current state: job state = '{jobContext.Result}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user