using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using GitHub.DistributedTask.WebApi; using GitHub.Runner.Common; using Newtonsoft.Json; namespace GitHub.Runner.Worker.Dap { /// /// Debug session state machine states. /// public enum DapSessionState { /// /// Initial state, waiting for client connection. /// WaitingForConnection, /// /// Client connected, exchanging capabilities. /// Initializing, /// /// ConfigurationDone received, ready to debug. /// Ready, /// /// Paused before or after a step, waiting for user command. /// Paused, /// /// Executing a step. /// Running, /// /// Session disconnected or terminated. /// Terminated } /// /// Commands that can be issued from the debug client. /// public enum DapCommand { /// /// Continue execution until end or next breakpoint. /// Continue, /// /// Execute current step and pause before next. /// Next, /// /// Pause execution. /// Pause, /// /// Disconnect from the debug session. /// Disconnect } /// /// Reasons for stopping/pausing execution. /// public static class StopReason { public const string Entry = "entry"; public const string Step = "step"; public const string Breakpoint = "breakpoint"; public const string Pause = "pause"; public const string Exception = "exception"; } /// /// Stores information about a completed step for stack trace display. /// internal sealed class CompletedStepInfo { public string DisplayName { get; set; } public TaskResult? Result { get; set; } public int FrameId { get; set; } } /// /// Interface for the DAP debug session. /// Handles debug state, step coordination, and DAP request processing. /// [ServiceLocator(Default = typeof(DapDebugSession))] public interface IDapDebugSession : IRunnerService { /// /// Gets whether the debug session is active (initialized and configured). /// bool IsActive { get; } /// /// Gets the current session state. /// DapSessionState State { get; } /// /// Sets the DAP server for sending events. /// /// The DAP server void SetDapServer(IDapServer server); /// /// Handles an incoming DAP request and returns a response. /// /// The DAP request /// The DAP response Task HandleRequestAsync(Request request); /// /// Called by StepsRunner before a step starts executing. /// May block waiting for debugger commands. /// /// The step about to execute /// The job execution context /// Whether this is the first step in the job /// Task that completes when execution should continue Task OnStepStartingAsync(IStep step, IExecutionContext jobContext, bool isFirstStep); /// /// Called by StepsRunner after a step completes. /// /// The step that completed void OnStepCompleted(IStep step); /// /// Notifies the session that the job has completed. /// void OnJobCompleted(); } /// /// Debug session implementation for handling DAP requests and coordinating /// step execution with the debugger. /// public sealed class DapDebugSession : RunnerService, IDapDebugSession { // Thread ID for the single job execution thread private const int JobThreadId = 1; // Frame ID base for the current step (always 1) private const int CurrentFrameId = 1; // Frame IDs for completed steps start at 1000 private const int CompletedFrameIdBase = 1000; private IDapServer _server; private DapSessionState _state = DapSessionState.WaitingForConnection; private InitializeRequestArguments _clientCapabilities; // Synchronization for step execution private TaskCompletionSource _commandTcs; private readonly object _stateLock = new object(); // Whether to pause before the next step (set by 'next' command) private bool _pauseOnNextStep = true; // Current execution context (set during OnStepStartingAsync) private IStep _currentStep; private IExecutionContext _jobContext; // Track completed steps for stack trace private readonly List _completedSteps = new List(); private int _nextCompletedFrameId = CompletedFrameIdBase; // Variable provider for converting contexts to DAP variables private DapVariableProvider _variableProvider; public bool IsActive => _state == DapSessionState.Ready || _state == DapSessionState.Paused || _state == DapSessionState.Running; public DapSessionState State => _state; public override void Initialize(IHostContext hostContext) { base.Initialize(hostContext); _variableProvider = new DapVariableProvider(hostContext); Trace.Info("DapDebugSession initialized"); } public void SetDapServer(IDapServer server) { _server = server; Trace.Info("DAP server reference set"); } public async Task HandleRequestAsync(Request request) { Trace.Info($"Handling DAP request: {request.Command}"); try { return request.Command switch { "initialize" => HandleInitialize(request), "attach" => HandleAttach(request), "configurationDone" => HandleConfigurationDone(request), "disconnect" => HandleDisconnect(request), "threads" => HandleThreads(request), "stackTrace" => HandleStackTrace(request), "scopes" => HandleScopes(request), "variables" => HandleVariables(request), "continue" => HandleContinue(request), "next" => HandleNext(request), "pause" => HandlePause(request), "evaluate" => await HandleEvaluateAsync(request), "setBreakpoints" => HandleSetBreakpoints(request), "setExceptionBreakpoints" => HandleSetExceptionBreakpoints(request), _ => CreateErrorResponse($"Unknown command: {request.Command}") }; } catch (Exception ex) { Trace.Error($"Error handling request '{request.Command}': {ex}"); return CreateErrorResponse(ex.Message); } } #region DAP Request Handlers private Response HandleInitialize(Request request) { // Parse client capabilities if (request.Arguments != null) { _clientCapabilities = request.Arguments.ToObject(); Trace.Info($"Client: {_clientCapabilities.ClientName ?? _clientCapabilities.ClientId ?? "unknown"}"); } _state = DapSessionState.Initializing; // Build our capabilities response var capabilities = new Capabilities { SupportsConfigurationDoneRequest = true, SupportsEvaluateForHovers = true, SupportTerminateDebuggee = true, SupportsTerminateRequest = true, // We don't support these features (yet) SupportsStepBack = false, SupportsSetVariable = false, SupportsRestartFrame = false, SupportsGotoTargetsRequest = false, SupportsStepInTargetsRequest = false, SupportsCompletionsRequest = false, SupportsModulesRequest = false, SupportsFunctionBreakpoints = false, SupportsConditionalBreakpoints = false, SupportsExceptionOptions = false, SupportsValueFormattingOptions = false, SupportsExceptionInfoRequest = false, SupportsDelayedStackTraceLoading = false, SupportsLoadedSourcesRequest = false, SupportsProgressReporting = false, SupportsRunInTerminalRequest = false, SupportsCancelRequest = false, }; // Queue the initialized event to be sent after the response Task.Run(() => { // Small delay to ensure response is sent first System.Threading.Thread.Sleep(50); _server?.SendEvent(new Event { EventType = "initialized" }); Trace.Info("Sent initialized event"); }); Trace.Info("Initialize request handled, capabilities sent"); return CreateSuccessResponse(capabilities); } private Response HandleAttach(Request request) { Trace.Info("Attach request handled"); return CreateSuccessResponse(null); } private Response HandleConfigurationDone(Request request) { lock (_stateLock) { _state = DapSessionState.Ready; } Trace.Info("Configuration done, debug session is ready"); // Complete any pending wait for configuration return CreateSuccessResponse(null); } private Response HandleDisconnect(Request request) { Trace.Info("Disconnect request received"); lock (_stateLock) { _state = DapSessionState.Terminated; // Release any blocked step execution _commandTcs?.TrySetResult(DapCommand.Disconnect); } return CreateSuccessResponse(null); } private Response HandleThreads(Request request) { // We have a single thread representing the job execution var body = new ThreadsResponseBody { Threads = new List { new Thread { Id = JobThreadId, Name = _jobContext != null ? $"Job: {_jobContext.GetGitHubContext("job") ?? "workflow job"}" : "Job Thread" } } }; return CreateSuccessResponse(body); } private Response HandleStackTrace(Request request) { var args = request.Arguments?.ToObject(); var frames = new List(); // Add current step as the top frame if (_currentStep != null) { var resultIndicator = _currentStep.ExecutionContext?.Result != null ? $" [{_currentStep.ExecutionContext.Result}]" : " [running]"; frames.Add(new StackFrame { Id = CurrentFrameId, Name = $"{_currentStep.DisplayName ?? "Current Step"}{resultIndicator}", Line = 1, Column = 1, PresentationHint = "normal" }); } else { frames.Add(new StackFrame { Id = CurrentFrameId, Name = "(no step executing)", Line = 1, Column = 1, PresentationHint = "subtle" }); } // Add completed steps as additional frames (most recent first) for (int i = _completedSteps.Count - 1; i >= 0; i--) { var completedStep = _completedSteps[i]; var resultStr = completedStep.Result.HasValue ? $" [{completedStep.Result}]" : ""; frames.Add(new StackFrame { Id = completedStep.FrameId, Name = $"{completedStep.DisplayName}{resultStr}", Line = 1, Column = 1, PresentationHint = "subtle" }); } var body = new StackTraceResponseBody { StackFrames = frames, TotalFrames = frames.Count }; return CreateSuccessResponse(body); } private Response HandleScopes(Request request) { var args = request.Arguments?.ToObject(); var frameId = args?.FrameId ?? CurrentFrameId; // Get the execution context for the requested frame var context = GetExecutionContextForFrame(frameId); if (context == null) { // Return empty scopes if no context available return CreateSuccessResponse(new ScopesResponseBody { Scopes = new List() }); } // Use the variable provider to get scopes var scopes = _variableProvider.GetScopes(context, frameId); return CreateSuccessResponse(new ScopesResponseBody { Scopes = scopes }); } private Response HandleVariables(Request request) { var args = request.Arguments?.ToObject(); var variablesRef = args?.VariablesReference ?? 0; // Get the current execution context var context = _currentStep?.ExecutionContext ?? _jobContext; if (context == null) { return CreateSuccessResponse(new VariablesResponseBody { Variables = new List() }); } // Use the variable provider to get variables var variables = _variableProvider.GetVariables(context, variablesRef); return CreateSuccessResponse(new VariablesResponseBody { Variables = variables }); } private Response HandleContinue(Request request) { Trace.Info("Continue command received"); lock (_stateLock) { if (_state == DapSessionState.Paused) { _state = DapSessionState.Running; _pauseOnNextStep = false; // Don't pause on next step _commandTcs?.TrySetResult(DapCommand.Continue); } } return CreateSuccessResponse(new ContinueResponseBody { AllThreadsContinued = true }); } private Response HandleNext(Request request) { Trace.Info("Next (step over) command received"); lock (_stateLock) { if (_state == DapSessionState.Paused) { _state = DapSessionState.Running; _pauseOnNextStep = true; // Pause before next step _commandTcs?.TrySetResult(DapCommand.Next); } } return CreateSuccessResponse(null); } private Response HandlePause(Request request) { Trace.Info("Pause command received"); // The runner will pause at the next step boundary lock (_stateLock) { _pauseOnNextStep = true; } return CreateSuccessResponse(null); } private Task HandleEvaluateAsync(Request request) { var args = request.Arguments?.ToObject(); var expression = args?.Expression ?? ""; var context = args?.Context ?? "hover"; Trace.Info($"Evaluate: '{expression}' (context: {context})"); // Stub implementation - Phase 4 will implement expression evaluation var body = new EvaluateResponseBody { Result = $"(evaluation of '{expression}' will be implemented in Phase 4)", Type = "string", VariablesReference = 0 }; return Task.FromResult(CreateSuccessResponse(body)); } private Response HandleSetBreakpoints(Request request) { // Stub - breakpoints not implemented in demo Trace.Info("SetBreakpoints request (not implemented)"); return CreateSuccessResponse(new { breakpoints = new object[0] }); } private Response HandleSetExceptionBreakpoints(Request request) { // Stub - exception breakpoints not implemented in demo Trace.Info("SetExceptionBreakpoints request (not implemented)"); return CreateSuccessResponse(new { breakpoints = new object[0] }); } #endregion #region Step Coordination (called by StepsRunner) public async Task OnStepStartingAsync(IStep step, IExecutionContext jobContext, bool isFirstStep) { if (!IsActive) { return; } _currentStep = step; _jobContext = jobContext; // Reset variable provider state for new step context _variableProvider.Reset(); // Determine if we should pause bool shouldPause = isFirstStep || _pauseOnNextStep; if (!shouldPause) { Trace.Info($"Step starting (not pausing): {step.DisplayName}"); return; } var reason = isFirstStep ? StopReason.Entry : StopReason.Step; var description = isFirstStep ? $"Stopped at job entry: {step.DisplayName}" : $"Stopped before step: {step.DisplayName}"; Trace.Info($"Step starting: {step.DisplayName} (reason: {reason})"); // Send stopped event to debugger _server?.SendEvent(new Event { EventType = "stopped", Body = new StoppedEventBody { Reason = reason, Description = description, ThreadId = JobThreadId, AllThreadsStopped = true } }); // Wait for debugger command await WaitForCommandAsync(); } public void OnStepCompleted(IStep step) { if (!IsActive) { return; } var result = step.ExecutionContext?.Result; Trace.Info($"Step completed: {step.DisplayName}, result: {result}"); // Add to completed steps list _completedSteps.Add(new CompletedStepInfo { DisplayName = step.DisplayName, Result = result, FrameId = _nextCompletedFrameId++ }); // Clear current step reference since it's done // (will be set again when next step starts) } public void OnJobCompleted() { if (!IsActive) { return; } Trace.Info("Job completed, sending terminated event"); lock (_stateLock) { _state = DapSessionState.Terminated; } // Send terminated event _server?.SendEvent(new Event { EventType = "terminated", Body = new TerminatedEventBody() }); // Send exited event var exitCode = _jobContext?.Result == TaskResult.Succeeded ? 0 : 1; _server?.SendEvent(new Event { EventType = "exited", Body = new ExitedEventBody { ExitCode = exitCode } }); } private async Task WaitForCommandAsync() { lock (_stateLock) { _state = DapSessionState.Paused; _commandTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } Trace.Info("Waiting for debugger command..."); var command = await _commandTcs.Task; Trace.Info($"Received command: {command}"); lock (_stateLock) { if (_state == DapSessionState.Paused) { _state = DapSessionState.Running; } } // Send continued event if (command == DapCommand.Continue || command == DapCommand.Next) { _server?.SendEvent(new Event { EventType = "continued", Body = new ContinuedEventBody { ThreadId = JobThreadId, AllThreadsContinued = true } }); } } /// /// Gets the execution context for a given frame ID. /// Currently only supports the current frame (completed steps don't have saved contexts). /// private IExecutionContext GetExecutionContextForFrame(int frameId) { if (frameId == CurrentFrameId) { return _currentStep?.ExecutionContext ?? _jobContext; } // For completed steps, we would need to save their execution contexts // For now, return null (variables won't be available for completed steps) return null; } #endregion #region Response Helpers private Response CreateSuccessResponse(object body) { return new Response { Success = true, Body = body }; } private Response CreateErrorResponse(string message) { return new Response { Success = false, Message = message, Body = new ErrorResponseBody { Error = new Message { Id = 1, Format = message, ShowUser = true } } }; } #endregion } }