From cc51348c56a227cff6125fb532589fdb2f675a41 Mon Sep 17 00:00:00 2001 From: Lex Li Date: Sat, 2 May 2026 00:53:38 -0400 Subject: [PATCH 1/5] Add exception handling support and state management in ManagedDebugger --- src/SharpDbg.Application/DebugAdapter.cs | 32 ++++ .../Debugger/ManagedDebugger.cs | 1 + .../Debugger/ManagedDebugger_EventHandlers.cs | 161 +++++++++++++++++- .../Debugger/ManagedDebugger_ExceptionInfo.cs | 59 +++++++ .../ManagedDebugger_RequestHandlers.cs | 90 +++++++++- 5 files changed, 336 insertions(+), 7 deletions(-) create mode 100644 src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ExceptionInfo.cs diff --git a/src/SharpDbg.Application/DebugAdapter.cs b/src/SharpDbg.Application/DebugAdapter.cs index 4b335ca..91a1c16 100644 --- a/src/SharpDbg.Application/DebugAdapter.cs +++ b/src/SharpDbg.Application/DebugAdapter.cs @@ -187,6 +187,7 @@ protected override InitializeResponse HandleInitializeRequest(InitializeArgument SupportsConditionalBreakpoints = true, SupportsHitConditionalBreakpoints = true, SupportsEvaluateForHovers = true, + SupportsExceptionInfoRequest = true, SupportsStepBack = false, SupportsSetVariable = false, SupportsRestartFrame = false, @@ -352,6 +353,37 @@ protected override StackTraceResponse HandleStackTraceRequest(StackTraceArgument }); } + protected override ExceptionInfoResponse HandleExceptionInfoRequest(ExceptionInfoArguments arguments) + { + return ExecuteWithExceptionHandling(() => + { + if (arguments == null || arguments.ThreadId == 0) + { + throw new ProtocolException("Missing thread id"); + } + var threadId = arguments.ThreadId; + var ex = _debugger.GetExceptionInfoForThread(threadId); + if (ex is null) + { + var id = $"ex-{threadId}"; + var resp = new ExceptionInfoResponse(id, ExceptionBreakMode.Unhandled); + resp.Description = $"Exception on thread {threadId}"; + return resp; + } + var details = new ExceptionDetails(); + details.Message = ex.Message ?? string.Empty; + details.TypeName = ex.TypeName ?? ex.FullTypeName ?? "Exception"; + details.FullTypeName = ex.FullTypeName ?? ex.TypeName ?? string.Empty; + details.EvaluateName = ex.EvaluateName ?? null; + details.StackTrace = ex.StackTrace ?? string.Empty; + details.FormattedDescription = ex.Message ?? string.Empty; + var resp2 = new ExceptionInfoResponse(ex.ExceptionId, ExceptionBreakMode.Unhandled); + resp2.Description = ex.Message ?? string.Empty; + resp2.Details = details; + return resp2; + }); + } + protected override ScopesResponse HandleScopesRequest(ScopesArguments arguments) { return ExecuteWithExceptionHandling(() => diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs index be46827..b108e6c 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs @@ -69,6 +69,7 @@ private void OnAnyEvent(object? sender, CorDebugManagedCallbackEventArgs e) case StepCompleteCorDebugManagedCallbackEventArgs a: HandleStepComplete(sender, a); break; case BreakCorDebugManagedCallbackEventArgs a: HandleBreak(sender, a); break; case ExceptionCorDebugManagedCallbackEventArgs a: HandleException(sender, a); break; + case Exception2CorDebugManagedCallbackEventArgs a: HandleException2(sender, a); break; case EvalCompleteCorDebugManagedCallbackEventArgs or EvalExceptionCorDebugManagedCallbackEventArgs: break; // don't continue on these, as they are being used for expression evaluation default: e.Controller.Continue(false); break; } diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs index 2da0105..e25c014 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Reflection.PortableExecutable; using ClrDebug; using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator; using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator.Interpreter; @@ -254,14 +255,15 @@ private void HandleBreak(object? sender, OnStopped?.Invoke(corThread.Id, "pause"); } - private void HandleException(object? sender, ExceptionCorDebugManagedCallbackEventArgs exceptionCorDebugManagedCallbackEventArgs) + private void HandleException(object? sender, ExceptionCorDebugManagedCallbackEventArgs ev) { if (EvalStatus.IsRunning) { ContinueProcess(); return; } - var corThread = exceptionCorDebugManagedCallbackEventArgs.Thread; + + var corThread = ev.Thread; IsRunning = false; _asyncStepper?.Disable(); if (_stepper is not null) @@ -270,6 +272,161 @@ private void HandleException(object? sender, ExceptionCorDebugManagedCallbackEve _stepper = null; } + try + { + var frames = GetStackTrace(corThread.Id); + var stackTrace = string.Join("\n", frames.Select(f => f.Name + (f.Source != null ? $" at {f.Source}:{f.Line}" : string.Empty))); + + CorDebugValue? exceptionValue = null; + string? typeName = null; + string? fullTypeName = null; + string? message = null; + + try + { + exceptionValue = corThread.CurrentException; + } + catch (Exception ex2) + { + _logger?.Invoke($"Error getting current exception: {ex2.Message}"); + } + + if (exceptionValue is not null) + { + try + { + var objectValue = exceptionValue.UnwrapDebugValueToObject(); + fullTypeName = GetCorDebugTypeFriendlyName(objectValue.ExactType); + var lastDot = fullTypeName.LastIndexOf('.'); + typeName = lastDot >= 0 ? fullTypeName.Substring(lastDot + 1) : fullTypeName; + message = TryReadExceptionMessage(objectValue); + } + catch (Exception ex3) + { + _logger?.Invoke($"Error reading exception details: {ex3.Message}"); + } + } + + if (fullTypeName != null) + _logger?.Invoke($"Unhandled exception: {fullTypeName}{(message != null ? $": {message}" : "")}"); + _logger?.Invoke($"Exception call stack:\n{stackTrace}"); + + StoreExceptionForThread(corThread.Id, message, typeName, fullTypeName, $"$exception{corThread.Id}", stackTrace, exceptionValue); + } + catch (Exception ex) + { + _logger?.Invoke($"Error capturing exception info: {ex.Message}"); + StoreExceptionForThread(corThread.Id, null, null, null, null, null, null); + } + OnStopped?.Invoke(corThread.Id, "exception"); } + + private void HandleException2(object? sender, Exception2CorDebugManagedCallbackEventArgs ev) + { + var corThread = ev.Thread; + IsRunning = false; + _asyncStepper?.Disable(); + if (_stepper is not null) + { + _stepper.Deactivate(); + _stepper = null; + } + + try + { + var frames = GetStackTrace(corThread.Id); + var stackTrace = string.Join("\n", frames.Select(f => f.Name + (f.Source != null ? $" at {f.Source}:{f.Line}" : string.Empty))); + + CorDebugValue? exceptionValue = null; + string? typeName = null; + string? fullTypeName = null; + string? message = null; + + try + { + exceptionValue = corThread.CurrentException; + } + catch (Exception ex2) + { + _logger?.Invoke($"Error getting current exception: {ex2.Message}"); + } + + if (exceptionValue is not null) + { + try + { + var objectValue = exceptionValue.UnwrapDebugValueToObject(); + fullTypeName = GetCorDebugTypeFriendlyName(objectValue.ExactType); + var lastDot = fullTypeName.LastIndexOf('.'); + typeName = lastDot >= 0 ? fullTypeName.Substring(lastDot + 1) : fullTypeName; + message = TryReadExceptionMessage(objectValue); + } + catch (Exception ex3) + { + _logger?.Invoke($"Error reading exception details: {ex3.Message}"); + } + } + + if (fullTypeName != null) + _logger?.Invoke($"Unhandled exception: {fullTypeName}{(message != null ? $": {message}" : "")} ({ev.EventType})"); + _logger?.Invoke($"Exception call stack:\n{stackTrace}"); + + StoreExceptionForThread(corThread.Id, message, typeName, fullTypeName, $"$exception{corThread.Id}", stackTrace, exceptionValue); + } + catch (Exception ex) + { + _logger?.Invoke($"Error capturing exception info (Exception2): {ex.Message}"); + StoreExceptionForThread(corThread.Id, null, null, null, null, null, null); + } + + OnStopped?.Invoke(corThread.Id, "exception"); + } + + private static string? TryReadExceptionMessage(CorDebugObjectValue exObj) + { + var currentType = exObj.ExactType; + while (currentType?.Class != null) + { + try + { + var metadataImport = currentType.Class.Module.GetMetaDataInterface().MetaDataImport; + var fieldDef = metadataImport.EnumFieldsWithName(currentType.Class.Token, "_message").FirstOrDefault(); + if (!fieldDef.IsNil) + { + var fieldValue = exObj.GetFieldValue(currentType.Class.Raw, fieldDef); + var unwrapped = fieldValue.UnwrapDebugValue(); + if (unwrapped is CorDebugStringValue sv) + return sv.GetStringWithoutBug(sv.Length + 1); + } + } + catch { } + currentType = currentType.Base; + } + return null; + } + + private void TryArmStopAtEntryBreakpoint(ModuleInfo moduleInfo) + { + try + { + using var stream = File.OpenRead(moduleInfo.ModulePath); + using var peReader = new PEReader(stream); + var corHeader = peReader.PEHeaders.CorHeader; + if (corHeader == null || corHeader.EntryPointTokenOrRelativeVirtualAddress == 0) + { + _logger?.Invoke($"No managed entry point found for {moduleInfo.ModulePath}"); + return; + } + + var function = moduleInfo.Module.GetFunctionFromToken(corHeader.EntryPointTokenOrRelativeVirtualAddress); + var breakpoint = function.ILCode.CreateBreakpoint(0); + breakpoint.Activate(true); + _logger?.Invoke($"Armed stopAtEntry breakpoint in {moduleInfo.ModuleName} at token 0x{corHeader.EntryPointTokenOrRelativeVirtualAddress:X}"); + } + catch (Exception ex) + { + _logger?.Invoke($"Could not arm stopAtEntry breakpoint for {moduleInfo.ModulePath}: {ex.Message}"); + } + } } diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ExceptionInfo.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ExceptionInfo.cs new file mode 100644 index 0000000..460bf1e --- /dev/null +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_ExceptionInfo.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ClrDebug; + +namespace SharpDbg.Infrastructure.Debugger; + +public partial class ManagedDebugger +{ + // Minimal per-thread exception state stored when an exception callback occurs. + private readonly Dictionary _threadExceptions = new(); + + public record ExceptionState(string ExceptionId, string? Message, string? TypeName, string? FullTypeName, string? EvaluateName, string? StackTrace, CorDebugValue? ExceptionValue); + + public ExceptionState? GetExceptionInfoForThread(int threadId) + { + lock (_threadExceptions) + { + _threadExceptions.TryGetValue(threadId, out var state); + return state; + } + } + + private void StoreExceptionForThread(int threadId, string? message, string? typeName, string? fullTypeName, string? evaluateName, string? stackTrace, CorDebugValue? exceptionValue) + { + var id = Guid.NewGuid().ToString(); + var state = new ExceptionState(id, message, typeName, fullTypeName, evaluateName, stackTrace, exceptionValue); + lock (_threadExceptions) + { + _threadExceptions[threadId] = state; + } + + // Log stored exception metadata for diagnostics + try + { + _logger?.Invoke($"Stored exception for thread {threadId}: hasValue={(exceptionValue is not null)}, isHandle={(exceptionValue is CorDebugHandleValue)}"); + } + catch + { + // ignore logging failures + } + } + + private void ClearAllThreadExceptions() + { + lock (_threadExceptions) + { + // Dispose any handle values if necessary + foreach (var s in _threadExceptions.Values) + { + if (s.ExceptionValue is CorDebugHandleValue hv) + { + try { hv.Dispose(); } catch { } + } + } + _threadExceptions.Clear(); + } + } +} diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs index 405235b..149ab06 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs @@ -56,6 +56,16 @@ private void PerformLaunch() _pendingLaunchArgs = null; _pendingLaunchWorkingDirectory = null; + // On non-Windows, .dll/.exe assemblies cannot be exec'd directly; wrap with dotnet host + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + (program.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || + program.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))) + { + _logger?.Invoke($"Non-Windows: prepending dotnet host for assembly launch"); + args = args != null ? [program, ..args] : [program]; + program = "dotnet"; + } + // Build command line: "program" "arg1" "arg2" ... var commandLine = new StringBuilder(); commandLine.Append('"').Append(program).Append('"'); @@ -68,14 +78,24 @@ private void PerformLaunch() // Initialize DbgShim var dbgShimPath = DbgShimResolver.Resolve(); + _logger?.Invoke($"DbgShim path: {dbgShimPath}"); var dbgshim = new DbgShim(NativeLibrary.Load(dbgShimPath)); // Create process suspended - var result = dbgshim.CreateProcessForLaunch( - commandLine.ToString(), - bSuspendProcess: true, - lpEnvironment: IntPtr.Zero, // TODO: support environment variables - lpCurrentDirectory: workingDirectory); + CreateProcessForLaunchResult result; + try + { + result = dbgshim.CreateProcessForLaunch( + commandLine.ToString(), + bSuspendProcess: true, + lpEnvironment: IntPtr.Zero, // TODO: support environment variables + lpCurrentDirectory: workingDirectory); + } + catch (Exception ex) + { + _logger?.Invoke($"CreateProcessForLaunch failed: {ex.GetType().Name}: {ex.Message}"); + throw; + } var processId = result.ProcessId; var resumeHandle = result.ResumeHandle; @@ -194,6 +214,9 @@ public void Continue() _logger?.Invoke("Continue"); if (_rawProcess != null) { + // Clear any cached exception info when execution is resumed + ClearAllThreadExceptions(); + IsRunning = true; _variableManager.ClearAndDisposeHandleValues(); _rawProcess.Continue(false); @@ -483,6 +506,21 @@ public List GetScopes(int frameId) VariablesReference = localsRef, Expensive = false }); + + // If an exception was recorded for this thread, expose it as an "Exception" scope + var excState = GetExceptionInfoForThread(variablesReference.Value.ThreadId.Value); + if (excState is not null && excState.ExceptionValue is not null) + { + var excRef = _variableManager.CreateReference(new VariablesReference(StoredReferenceKind.StackVariable, excState.ExceptionValue, variablesReference.Value.ThreadId, variablesReference.Value.FrameStackDepth, null)); + _logger?.Invoke($"Created Exception scope variablesReference={excRef} for thread {variablesReference.Value.ThreadId.Value}"); + result.Add(new ScopeInfo + { + Name = "Exception", + VariablesReference = excRef, + Expensive = false + }); + } + return result; } @@ -560,6 +598,48 @@ public async Task> GetVariables(int variablesReferenceInt) public async Task<(string result, string? type, int variablesReference)> Evaluate(string expression, int? frameId) { _logger?.Invoke($"Evaluate: {expression}"); + + // Special-case: evaluate the stored exception object for a thread using the synthetic expression + if (!string.IsNullOrEmpty(expression) && expression.StartsWith("$exception")) + { + // expression may be "$exception" or "$exception{threadId}" + var suffix = expression.Substring("$exception".Length); + int parsedThreadId = 0; + if (!string.IsNullOrEmpty(suffix) && int.TryParse(suffix, out var tmp)) parsedThreadId = tmp; + + ExceptionState? state = null; + if (parsedThreadId != 0) + { + state = GetExceptionInfoForThread(parsedThreadId); + } + else if (frameId.HasValue && frameId.Value != 0) + { + var frameRef = _variableManager.GetReference(frameId.Value); + if (frameRef.HasValue && frameRef.Value.ThreadId.Value > 0) + { + state = GetExceptionInfoForThread(frameRef.Value.ThreadId.Value); + } + } + + if (state is not null && state.ExceptionValue is not null) + { + int targetThreadId = parsedThreadId; + if (targetThreadId == 0 && frameId.HasValue && frameId.Value != 0) + { + var frameRef = _variableManager.GetReference(frameId.Value); + if (frameRef.HasValue && frameRef.Value.ThreadId.Value > 0) + { + targetThreadId = frameRef.Value.ThreadId.Value; + } + } + var exceptionVariablesReference = GenerateUniqueVariableReference(state.ExceptionValue, new ThreadId(targetThreadId), new FrameStackDepth(0), null); + var exceptionFriendlyTypeName = state.FullTypeName ?? state.TypeName ?? "Exception"; + var resultString = state.Message ?? exceptionFriendlyTypeName; + return (resultString, exceptionFriendlyTypeName, exceptionVariablesReference); + } + return ("", null, 0); + } + if (frameId is null or 0) throw new InvalidOperationException("Frame ID is required for evaluation"); var variablesReference = _variableManager.GetReference(frameId.Value); From e02e78282aca69fec0d7737eac528b80c99b0edc Mon Sep 17 00:00:00 2001 From: Lex Li <425130+lextm@users.noreply.github.com> Date: Sat, 2 May 2026 00:53:54 -0400 Subject: [PATCH 2/5] Update src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs Co-authored-by: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> --- .../Debugger/ManagedDebugger_RequestHandlers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs index 149ab06..26359d1 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs @@ -603,9 +603,9 @@ public async Task> GetVariables(int variablesReferenceInt) if (!string.IsNullOrEmpty(expression) && expression.StartsWith("$exception")) { // expression may be "$exception" or "$exception{threadId}" - var suffix = expression.Substring("$exception".Length); + var suffix = expression.AsSpan()["$exception".Length..]; int parsedThreadId = 0; - if (!string.IsNullOrEmpty(suffix) && int.TryParse(suffix, out var tmp)) parsedThreadId = tmp; + if (suffix.IsEmpty is false && int.TryParse(suffix, out var tmp)) parsedThreadId = tmp; ExceptionState? state = null; if (parsedThreadId != 0) From 63f08ccc152dc1ca651e3c57cbef83851bb00d0e Mon Sep 17 00:00:00 2001 From: Lex Li <425130+lextm@users.noreply.github.com> Date: Sat, 2 May 2026 00:53:55 -0400 Subject: [PATCH 3/5] Update src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs Co-authored-by: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> --- .../Debugger/ManagedDebugger_RequestHandlers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs index 26359d1..1d7d280 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs @@ -57,7 +57,7 @@ private void PerformLaunch() _pendingLaunchWorkingDirectory = null; // On non-Windows, .dll/.exe assemblies cannot be exec'd directly; wrap with dotnet host - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) is false && (program.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || program.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))) { From 0e8c59fb3b2a43ced1cdb8879d7a37a91672942c Mon Sep 17 00:00:00 2001 From: Lex Li Date: Sat, 2 May 2026 00:53:55 -0400 Subject: [PATCH 4/5] Address review feedback Co-authored-by: Copilot --- .../Debugger/ManagedDebugger_EventHandlers.cs | 24 ------------------- .../ManagedDebugger_RequestHandlers.cs | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs index e25c014..b94cd9a 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs @@ -405,28 +405,4 @@ private void HandleException2(object? sender, Exception2CorDebugManagedCallbackE } return null; } - - private void TryArmStopAtEntryBreakpoint(ModuleInfo moduleInfo) - { - try - { - using var stream = File.OpenRead(moduleInfo.ModulePath); - using var peReader = new PEReader(stream); - var corHeader = peReader.PEHeaders.CorHeader; - if (corHeader == null || corHeader.EntryPointTokenOrRelativeVirtualAddress == 0) - { - _logger?.Invoke($"No managed entry point found for {moduleInfo.ModulePath}"); - return; - } - - var function = moduleInfo.Module.GetFunctionFromToken(corHeader.EntryPointTokenOrRelativeVirtualAddress); - var breakpoint = function.ILCode.CreateBreakpoint(0); - breakpoint.Activate(true); - _logger?.Invoke($"Armed stopAtEntry breakpoint in {moduleInfo.ModuleName} at token 0x{corHeader.EntryPointTokenOrRelativeVirtualAddress:X}"); - } - catch (Exception ex) - { - _logger?.Invoke($"Could not arm stopAtEntry breakpoint for {moduleInfo.ModulePath}: {ex.Message}"); - } - } } diff --git a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs index 1d7d280..3a55f44 100644 --- a/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs +++ b/src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_RequestHandlers.cs @@ -62,7 +62,7 @@ private void PerformLaunch() program.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))) { _logger?.Invoke($"Non-Windows: prepending dotnet host for assembly launch"); - args = args != null ? [program, ..args] : [program]; + args = args.Length > 0 ? [program, ..args] : [program]; program = "dotnet"; } From d2946b547c13914dc96b9557d9240e4b6184294c Mon Sep 17 00:00:00 2001 From: Lex Li Date: Sat, 2 May 2026 00:53:55 -0400 Subject: [PATCH 5/5] Allow sharpdbg to be submodule of other repos --- tests/SharpDbg.Cli.Tests/Helpers/GitRoot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SharpDbg.Cli.Tests/Helpers/GitRoot.cs b/tests/SharpDbg.Cli.Tests/Helpers/GitRoot.cs index 8e4f3c8..d8e4a64 100644 --- a/tests/SharpDbg.Cli.Tests/Helpers/GitRoot.cs +++ b/tests/SharpDbg.Cli.Tests/Helpers/GitRoot.cs @@ -8,7 +8,7 @@ public static string GetGitRootPath() if (_gitRoot is not null) return _gitRoot; var currentDirectory = Directory.GetCurrentDirectory(); var gitRoot = currentDirectory; - while (!Directory.Exists(Path.Combine(gitRoot, ".git"))) + while (!Directory.Exists(Path.Combine(gitRoot, ".git")) && !File.Exists(Path.Combine(gitRoot, ".git"))) { gitRoot = Path.GetDirectoryName(gitRoot); // parent directory if (string.IsNullOrWhiteSpace(gitRoot))