Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Debugging visualizer which targets DateTime doesn't transfer the value

I've written a Visual Studio debugging visualizer which targets DateTime (repo). My issue is that the debugger side only passes the target value to the debuggee side if the target expression is of object, not of DateTime (issue).

I've published a GH repo containing an MCVE that reproduces the problem. The debugger side looks like this:

protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider) {
    var response = objectProvider.TransferObject(5);

    var msg = response switch {
        string s => s,
        IEnumerable e => string.Join(", ", e.Cast<object>()),
        _ => "Unhandled type"
    };

    MessageBox.Show(msg);
}

and the debuggee side looks like this:

public override void TransferData(object target, Stream incomingData, Stream outgoingData) {
    int? repetitions = Deserialize(incomingData) switch {
        int i when i > 0 => i,
        string s when int.TryParse(s, out int i) && i > 0 => i,
        _ => null
    };

    object toSerialize =
        repetitions is null ? $"Invalid value for repetitions" :
        target switch {
            DateTime dt => Repeat(dt, repetitions.Value).ToArray(),
            null => $"{nameof(target)} is null",
            _ => $"Not implemented for target of type {target.GetType().FullName}" as object
        };

    Serialize(outgoingData, toSerialize);
}

After I build and install the visualizer, and begin debugging the following code:

var dte = DateTime.UtcNow;
object o = dte;

if I hover over o and trigger the visualizer, the target DateTime gets passed to the debuggee side, and returns an array of DateTime. But if I trigger the visualizer on dte, I get back the string target is null, implying the debugge side has received null in the target parameter.

What could be causing this? How could I resolve it?


Some random notes

  • It's not because the debugger side is always 32-bit and the debuggee side is sometimes 64-bit.
  • Nor is it because of different TFMs - when the debugger side targets .NET Framework, while the debuggee side can target .NET Standard or .NET Core.
  • Only the TransferData override is affected; the GetData override always gets the target value (I'm actually using GetData to workaround this, but I'd really rather use GetData for something else.) I've attempted to test ReplaceData / ReplaceObject, but the IsObjectReplaceable property always returns false.
  • I've tested against other value types -- TimeSpan, DateTimeOffset, and a custom struct -- and seen the same behavior. When I tested against int, however, the target process crashes and the debug session is interrupted.
  • Targeting against a DateTime? shows the same behavior as DateTime; I imagine this is because they're both serialized the same way.

Stack trace of the exception hit when similarly targeting an int

In response to this comment, the error message when visualizing an int is as follows:

The target process exited with code -1073740791 (0xC0000409) while evaluating the function 'Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost.TransferData'.

If the problem happens regularly, consider disabling the Tools->Options setting "Debugging->General->Enable property evaluation and other implicit function calls" or debugging the cause by evaluating the expression from the Immediate window. See help for information on doing this.

followed by another message:

Could not load the custom viewer.

upon which the target process crashes and the debugging session ends.

I tried unsuccessfully to attach a debugger using a code breakpoint (Debugger.Break()). If I return the call stack from the visualizer (new System.Diagnostics.StackTrace().ToString()) and the visualizer runs successfully, I get the following:

at SimpleValueTypeVisualizer.Debuggee.VisualizerObjectSource.TransferData(Object target, Stream incomingData, Stream outgoingData)

at Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost.TransferData(Object visualizedObject, Byte[] uiSideData)

at TestNoRef.Program.Main(String[] args)

which would seem to imply some exception at Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost.TransferData.

When I opened the DebuggerVisualizers.dll using ILSpy, the relevant TransferData method looks like this:

// Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost
using System.IO;

public byte[] TransferData(object visualizedObject, byte[] uiSideData)
{
    MemoryStream memoryStream = new MemoryStream();
    MemoryStream incomingData = ((uiSideData != null) ? new MemoryStream(uiSideData) : null);
    m_debuggeeSideVisualizerObject.TransferData(visualizedObject, incomingData, memoryStream);
    return memoryStream.ToArray();
}

I would guess the exception is at the third line of the method (MemoryStream incomingData = ...). But I am still unclear as to the details of the exception, particularly why the issue only arises with an unboxed value, and not with a boxed value.


Event log details

Per this comment, I am including data from the event log created when opening the visualizer on an expression of type int:

Log Name:      Application
Source:        Application Error
Date:          22/04/2021 12:14:36
Event ID:      1000
Task Category: (100)
Level:         Error
Keywords:      Classic
User:          N/A
Computer:      LAPTOP-7O43T4OO
Description:
Faulting application name: TestNoRef.exe, version: 1.0.0.0, time stamp: 0xd9f9e12d
Faulting module name: clr.dll, version: 4.8.4341.0, time stamp: 0x6023024f
Exception code: 0xc0000409
Fault offset: 0x00574845
Faulting process ID: 0x94c4
Faulting application start time: 0x01d73757c33e87c0
Faulting application path: ***********
Faulting module path: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
Report ID: 1dcf070b-71ff-4279-be71-822698cc6168
Faulting package full name: 
Faulting package-relative application ID: 
Event Xml:
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
  <System>
    <Provider Name="Application Error" />
    <EventID Qualifiers="0">1000</EventID>
    <Version>0</Version>
    <Level>2</Level>
    <Task>100</Task>
    <Opcode>0</Opcode>
    <Keywords>0x80000000000000</Keywords>
    <TimeCreated SystemTime="2021-04-22T09:14:36.4507272Z" />
    <EventRecordID>1180760705</EventRecordID>
    <Correlation />
    <Execution ProcessID="0" ThreadID="0" />
    <Channel>Application</Channel>
    <Computer>LAPTOP-7O43T4OO</Computer>
    <Security />
  </System>
  <EventData>
    <Data>TestNoRef.exe</Data>
    <Data>1.0.0.0</Data>
    <Data>d9f9e12d</Data>
    <Data>clr.dll</Data>
    <Data>4.8.4341.0</Data>
    <Data>6023024f</Data>
    <Data>c0000409</Data>
    <Data>00574845</Data>
    <Data>94c4</Data>
    <Data>01d73757c33e87c0</Data>
    <Data>***********</Data>
    <Data>C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll</Data>
    <Data>1dcf070b-71ff-4279-be71-822698cc6168</Data>
    <Data>
    </Data>
    <Data>
    </Data>
  </EventData>
</Event>
like image 801
Zev Spitz Avatar asked Apr 18 '21 19:04

Zev Spitz


People also ask

How can I see the variable value while debugging in Visual Studio?

When stopped in the debugger hover the mouse cursor over the variable you want to look at. The DataTip will appear showing you the value of that variable. If the variable is an object, you can expand the object by clicking on the arrow to see the elements of that object.

How do I keep debugging in Visual Studio?

In most languages supported by Visual Studio, you can edit your code in the middle of a debugging session and continue debugging. To use this feature, click into your code with your cursor while paused in the debugger, make edits, and press F5, F10, or F11 to continue debugging.

How do I set debug path in Visual Studio?

In Solution Explorer, right-click the project and choose Properties. In the side pane, choose Build (or Compile in Visual Basic). In the Configuration list at the top, choose Debug or Release. Select the Advanced button (or the Advanced Compile Options button in Visual Basic).


1 Answers

I can't find a proper solution. It could be just a bug introduced in one of the latest versions and nobody has come across this problem with value types so far. Actually, I tried

DateTime dte = DateTime.UtcNow;
ValueType vt = dte;

And again it does work with vt but not with dte. I added a explicit target to net48 just in case but it changed nothing.

The best I could come up with is a workaround very similar to the one I'm guessing Zev Spitz is using, but trying not to waste the GetData override just to get the target value. It's not a very nice solution, I'm afraid.

Provided that you want to use GetData to retrieve a different value but it will be used in your DialogDebuggerVisualizer.Show override, you can just store your value inside the VisualizerObjectSource object when you call GetData and retrieve it when you call TransferData, without actually trasferring it from Debuggee to Debugger.

 public class VisualizerObjectSource : Microsoft.VisualStudio.DebuggerVisualizers.VisualizerObjectSource 
    {
       /*static*/ DateTime? _lastDatetime=null;
        public override void TransferData(object target, Stream incomingData, Stream outgoingData) 
        {
            target = _lastDatetime;

            //Calculate here the output value        
            object toSerialize = " is null = " + (target==null).ToString();
       
            Serialize(outgoingData, toSerialize);
        }

        public override void GetData(object target, Stream outgoingData)
        {
            _lastDatetime = (DateTime)target;
            
            //Calculate here what you want to be returned by GetData
            base.GetData(" The stuff you want to return ", outgoingData);
        }   

    }

and in your Debugger side, make sure you call GetObject/GetData before you call TransferObject()


       protected override void Show(IDialogVisualizerService windowService, 
IVisualizerObjectProvider objectProvider)
        {            
            object MyCustomStuff =objectProvider.GetObject();
            var response = objectProvider.TransferObject(5);

           //[...]          

             string msg =  response .ToString();

            MessageBox.Show(msg);
        }

like image 121
Amo Robb Avatar answered Oct 16 '22 13:10

Amo Robb