diff --git a/src/SharpDbg.Application/DebugAdapter.cs b/src/SharpDbg.Application/DebugAdapter.cs index d18ccdb..fb11bcf 100644 --- a/src/SharpDbg.Application/DebugAdapter.cs +++ b/src/SharpDbg.Application/DebugAdapter.cs @@ -224,6 +224,7 @@ protected override LaunchResponse HandleLaunchRequest(LaunchArguments arguments) protected override AttachResponse HandleAttachRequest(AttachArguments arguments) { var processId = GetConfigValue(arguments.ConfigurationProperties, "processId"); + var stopAtEntry = GetConfigValue(arguments.ConfigurationProperties, "stopAtEntry") ?? false; if (processId == null) { throw new ProtocolException("Missing process ID"); @@ -231,7 +232,7 @@ protected override AttachResponse HandleAttachRequest(AttachArguments arguments) try { - _debugger.Attach(processId.Value); + _debugger.Attach(processId.Value, stopAtEntry); return new AttachResponse(); } catch (Exception ex) diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs index c2a2681..ecf6106 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs @@ -24,6 +24,8 @@ public partial class ManagedDebugger : IDisposable private int? _pendingAttachProcessId; private AsyncStepper? _asyncStepper; private CompiledExpressionInterpreter? _expressionInterpreter; + private bool _stopAtEntryActive; + private bool _stopAtEntrySignaled; public event Action? OnStopped; // ThreadId, FilePath, Line, Reason @@ -98,6 +100,11 @@ private void PerformAttach(int processId) private void ContinueProcess() { + if (_stopAtEntryActive && _stopAtEntrySignaled) + { + _logger?.Invoke("Auto-continue suppressed (stop-at-entry signaled)"); + return; + } if (_rawProcess != null) { IsRunning = true; diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs index 0d1766f..d16494d 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs @@ -10,6 +10,16 @@ private void HandleProcessCreated(object? sender, CreateProcessCorDebugManagedCa { _logger?.Invoke("Process created event"); _rawProcess = createProcessCorDebugManagedCallbackEventArgs.Process; + var corThread = _rawProcess.EnumerateThreads().FirstOrDefault(); + if (corThread != null) _threads.TryAdd(corThread.Id, corThread); + + if (_stopAtEntryActive && !_stopAtEntrySignaled && corThread != null) + { + _stopAtEntrySignaled = true; + IsRunning = false; + OnStopped?.Invoke(corThread.Id, "entry"); + return; + } ContinueProcess(); } @@ -25,8 +35,16 @@ private void HandleProcessExited(object? sender, ExitProcessCorDebugManagedCallb private void HandleThreadCreated(object? sender, CreateThreadCorDebugManagedCallbackEventArgs createThreadCorDebugManagedCallbackEventArgs) { var corThread = createThreadCorDebugManagedCallbackEventArgs.Thread; + var threadAlreadyKnown = _threads.ContainsKey(corThread.Id); _threads[corThread.Id] = corThread; - OnThreadStarted?.Invoke(corThread.Id, $"Thread {corThread.Id}"); + if (!threadAlreadyKnown) OnThreadStarted?.Invoke(corThread.Id, $"Thread {corThread.Id}"); + if (_stopAtEntryActive && !_stopAtEntrySignaled) + { + _stopAtEntrySignaled = true; + IsRunning = false; + OnStopped?.Invoke(corThread.Id, "entry"); + return; + } ContinueProcess(); } diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs index aab6954..cc53074 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs @@ -53,6 +53,8 @@ private void PerformLaunch() _pendingLaunchProgram = null; _pendingLaunchArgs = null; _pendingLaunchWorkingDirectory = null; + _stopAtEntryActive = stopAtEntry; + _stopAtEntrySignaled = false; // Build command line: "program" "arg1" "arg2" ... var commandLine = new StringBuilder(); @@ -156,10 +158,12 @@ public bool RemoveBreakpoint(int id) /// /// Store process ID for later attach (actual attach happens in ConfigurationDone) /// - public void Attach(int processId) + public void Attach(int processId, bool stopAtEntry = false) { _logger?.Invoke($"Storing attach target: {processId}"); _pendingAttachProcessId = processId; + _stopAtEntryActive = stopAtEntry; + _stopAtEntrySignaled = false; } /// @@ -191,6 +195,11 @@ public void Continue() _logger?.Invoke("Continue"); if (_rawProcess != null) { + if (_stopAtEntryActive) + { + _stopAtEntryActive = false; + _stopAtEntrySignaled = false; + } IsRunning = true; _variableManager.ClearAndDisposeHandleValues(); _rawProcess.Continue(false); @@ -217,6 +226,11 @@ public void Pause() public async void StepNext(int threadId) { _logger?.Invoke($"StepNext on thread {threadId}"); + if (_stopAtEntryActive) + { + _stopAtEntryActive = false; + _stopAtEntrySignaled = false; + } if (_threads.TryGetValue(threadId, out var thread)) { var frame = thread.ActiveFrame; @@ -250,6 +264,11 @@ public async void StepNext(int threadId) public async void StepIn(int threadId) { _logger?.Invoke($"StepIn on thread {threadId}"); + if (_stopAtEntryActive) + { + _stopAtEntryActive = false; + _stopAtEntrySignaled = false; + } if (_threads.TryGetValue(threadId, out var thread)) { var frame = thread.ActiveFrame; @@ -283,6 +302,11 @@ public async void StepIn(int threadId) public async void StepOut(int threadId) { _logger?.Invoke($"StepOut on thread {threadId}"); + if (_stopAtEntryActive) + { + _stopAtEntryActive = false; + _stopAtEntrySignaled = false; + } if (_threads.TryGetValue(threadId, out var thread)) { var frame = thread.ActiveFrame; diff --git a/tests/SharpDbg.Cli.Tests/Helpers/DebugAdapterProcessHelper.cs b/tests/SharpDbg.Cli.Tests/Helpers/DebugAdapterProcessHelper.cs index e47474f..7013570 100644 --- a/tests/SharpDbg.Cli.Tests/Helpers/DebugAdapterProcessHelper.cs +++ b/tests/SharpDbg.Cli.Tests/Helpers/DebugAdapterProcessHelper.cs @@ -72,7 +72,7 @@ public static InitializeRequest GetInitializeRequest() }; } - public static AttachRequest GetAttachRequest(int processId) + public static AttachRequest GetAttachRequest(int processId, bool stopAtEntry = false) { return new AttachRequest { @@ -81,6 +81,7 @@ public static AttachRequest GetAttachRequest(int processId) ["name"] = "AttachRequestName", ["type"] = "coreclr", ["processId"] = processId, + ["stopAtEntry"] = stopAtEntry, ["console"] = "internalConsole", // integratedTerminal, externalTerminal, internalConsole } }; diff --git a/tests/SharpDbg.Cli.Tests/StopAtEntryTests.cs b/tests/SharpDbg.Cli.Tests/StopAtEntryTests.cs new file mode 100644 index 0000000..00b39f0 --- /dev/null +++ b/tests/SharpDbg.Cli.Tests/StopAtEntryTests.cs @@ -0,0 +1,44 @@ +using AwesomeAssertions; +using Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.Messages; +using SharpDbg.Cli.Tests.Helpers; + +namespace SharpDbg.Cli.Tests; + +public class StopAtEntryTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + public async Task StopAtEntry_ResumeContinues_AndHitsBreakpoint() + { + var startSuspended = true; + + var (debugProtocolHost, initializedEventTcs, stoppedEventTcs, adapter, p2) = TestHelper.GetRunningDebugProtocolHostInProc(testOutputHelper, startSuspended); + using var _ = adapter; + using var __ = new ProcessKiller(p2); + + await debugProtocolHost + .WithInitializeRequest() + .WithAttachRequest(p2.Id, stopAtEntry: true) + .WaitForInitializedEvent(initializedEventTcs); + + // Compute a sensible breakpoint line inside Program.cs (the "Log2" WriteLine) + var programPath = Path.JoinFromGitRoot("tests", "DebuggableConsoleApp", "Program.cs"); + var lines = File.ReadAllLines(programPath); + var bpLine = Array.FindIndex(lines, l => l.Contains("Log2")) + 1; + if (bpLine == 0) bpLine = 4; // fallback + + debugProtocolHost.WithConfigurationDoneRequest() + .WithOptionalResumeRuntime(p2.Id, startSuspended); + + var entryStoppedEvent = await stoppedEventTcs.Tcs.Task.WaitAsync(TimeSpan.FromSeconds(15)); + stoppedEventTcs.Tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + entryStoppedEvent.Reason.Should().Be(StoppedEvent.ReasonValue.Entry); + + debugProtocolHost.WithBreakpointsRequest(bpLine, programPath) + .WithContinueRequest(); + + var stoppedEvent2 = await debugProtocolHost.WaitForStoppedEvent(stoppedEventTcs).WaitAsync(TimeSpan.FromSeconds(15)); + var stopInfo2 = stoppedEvent2.ReadStopInfo(); + stopInfo2.filePath.Should().EndWith("Program.cs"); + stopInfo2.line.Should().Be(bpLine); + } +} diff --git a/tests/SharpDbg.Cli.Tests/TestHelper.cs b/tests/SharpDbg.Cli.Tests/TestHelper.cs index b424f20..7c443b3 100644 --- a/tests/SharpDbg.Cli.Tests/TestHelper.cs +++ b/tests/SharpDbg.Cli.Tests/TestHelper.cs @@ -44,9 +44,9 @@ public static DebugProtocolHost WithInitializeRequest(this DebugProtocolHost deb return debugProtocolHost; } - public static DebugProtocolHost WithAttachRequest(this DebugProtocolHost debugProtocolHost, int debuggableProcessId) + public static DebugProtocolHost WithAttachRequest(this DebugProtocolHost debugProtocolHost, int debuggableProcessId, bool stopAtEntry = false) { - var attachRequest = DebugAdapterProcessHelper.GetAttachRequest(debuggableProcessId); + var attachRequest = DebugAdapterProcessHelper.GetAttachRequest(debuggableProcessId, stopAtEntry); debugProtocolHost.SendRequestSync(attachRequest); return debugProtocolHost; }