Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/SharpDbg.Application/DebugAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ protected override InitializeResponse HandleInitializeRequest(InitializeArgument
SupportsConditionalBreakpoints = true,
SupportsHitConditionalBreakpoints = true,
SupportsEvaluateForHovers = true,
SupportsExceptionInfoRequest = true,
SupportsStepBack = false,
SupportsSetVariable = false,
SupportsRestartFrame = false,
Expand Down Expand Up @@ -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(() =>
Expand Down
1 change: 1 addition & 0 deletions src/SharpDbg.Infrastructure/Debugger/ManagedDebugger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
137 changes: 135 additions & 2 deletions src/SharpDbg.Infrastructure/Debugger/ManagedDebugger_EventHandlers.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Reflection.PortableExecutable;
using ClrDebug;
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator;
using SharpDbg.Infrastructure.Debugger.ExpressionEvaluator.Interpreter;
Expand Down Expand Up @@ -254,14 +255,76 @@ 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)
{
_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}" : "")}");
_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)
Expand All @@ -270,6 +333,76 @@ 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}" : "")} ({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;
}
}
Original file line number Diff line number Diff line change
@@ -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<int, ExceptionState> _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();
}
}
}
Loading