微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

以 DateTime 为目标的调试可视化工具不传输值

如何解决以 DateTime 为目标的调试可视化工具不传输值

我写了一个 Visual Studio debugging visualizer,它针对 DateTime (repo)。我的问题是,如果目标表达式是 object,而不是 DateTime (issue),则调试器方仅将目标值传递给被调试方。

我发布了一个包含 MCVE that reproduces the problem 的 GH 存储库。调试器端看起来像这样:

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);
}

被调试方看起来像这样:

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);
}

在我构建并安装可视化工具后,开始调试以下代码

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

如果我将鼠标悬停在 o 上并触发可视化工具,目标 DateTime 将被传递到被调试方,并返回一个 DateTime 数组。但是,如果我在 dte 上触发可视化工具,我会返回字符串 target is null,这意味着调试端已收到 null 参数中的 target

这可能是什么原因造成的?我该如何解决


一些随机笔记

  • 这不是因为调试器端总是 32 位,而被调试器端有时是 64 位。
  • 也不是因为 TFM 不同 - 当调试器端面向 .NET Framework 时,而调试器端可以面向 .NET Standard 或 .NET Core。
  • 只有 TransferData 覆盖受到影响; GetData 覆盖总是获取目标值(我实际上是使用 GetData解决这个问题,但我真的更愿意将 GetData 用于其他用途。)我试图测试ReplaceData / ReplaceObject,但 IsObjectReplaceable 属性始终返回 false
  • 我已经针对其他值类型(TimeSpanDateTimeOffset自定义 struct)进行了测试,发现了相同的行为。但是,当我针对 int 进行测试时,目标进程崩溃并且调试会话中断。
  • 针对 DateTime? 显示DateTime 相同的行为;我想这是因为它们的序列化方式相同。

类似针对 int 时异常命中的堆栈跟踪

响应this comment,可视化int时的错误信息如下:

目标进程在评估函数“Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost.TransferData”时退出代码为 -1073740791 (0xC0000409)。

如果问题经常发生,请考虑禁用工具->选项设置“调试->常规->启用属性评估和其他隐式函数调用”或通过从立即窗口评估表达式来调试原因。有关执行此操作的信息,请参阅帮助。

接着是另一条消息:

无法加载自定义查看器。

目标进程崩溃,调试会话结束。

我尝试使用代码断点 (Debugger.Break()) 附加调试器失败。如果我从可视化器 (new System.Diagnostics.StackTrace().ToString()) 返回调用堆栈并且可视化器成功运行,我会得到以下信息:

在 SimpleValueTypeVisualizer.Debuggee.VisualizerObjectSource.TransferData(对象目标,流传入数据,流传出数据)

在 Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost.TransferData(Objectvisualizedobject,Byte[] uiSideData)

在 Testnoref.Program.Main(String[] args)

这似乎意味着 Microsoft.VisualStudio.DebuggerVisualizers.DebuggeeSide.Impl.ClrCustomVisualizerDebuggeeHost.TransferData 处有一些例外。

当我使用 ILSpy 打开 DebuggerVisualizers.dll 时,相关的 TransferData 方法如下所示:

// 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();
}

我猜想异常是在方法的第三行 (MemoryStream incomingData = ...)。但我仍然不清楚异常的细节,特别是为什么问题只出现在未装箱的值上,而不是装箱的值。


事件日志详情

根据 this comment,我将在打开可视化工具时创建的事件日志中的数据包含在 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>

解决方法

我找不到合适的解决方案。这可能只是一个最新版本中引入的错误,到目前为止没有人遇到过值类型的这个问题。其实我试过

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

它再次适用于 vt 但不适用于 dte。为了以防万一,我向 net48 添加了一个明确的目标,但它没有任何改变。

我能想到的最好的方法是一种与我猜测 Zev Spitz 正在使用的方法非常相似的解决方法,但尽量不要为了获得目标值而浪费 GetData 覆盖。恐怕这不是一个很好的解决方案。

如果您想使用 GetData 来检索不同的值,但它将在您的 DialogDebuggerVisualizer.Show 覆盖中使用,您可以在调用 GetData 时将您的值存储在 VisualizerObjectSource 对象中,并在调用 TransferData 时检索它,而无需实际上是将它从 Debuggee 传输到 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);
        }   

    }

并且在您的调试器端,请确保在调用 TransferObject() 之前调用 GetObject/GetData


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

           //[...]          

             string msg =  response .ToString();

            MessageBox.Show(msg);
        }

,

正如我在评论中提到的,TransferData 实际上不是必需的。此外,可以避免整个 BinaryFormatter 序列化(不幸的是,仅在将数据传输到调试器可视化器时,而不是在替换编辑值时,但还有另一个技巧)。

首先,考虑以下设置:

[assembly: DebuggerVisualizer(typeof(DateTimeVisualizer),typeof(DateTimeSerializer),Target = typeof(DateTime),Description = "DateTime Debugger Visualizer")]

其中序列化器如下:

// Note that TransferData is not overridden,and we do not call base.GetData so we can 
// avoid using BinaryFormatter (which often has issues when debugging a .NET Core or newer project)
internal class DateTimeSerializer : VisualizerObjectSource
{
    public override void GetData(object target,Stream outgoingData)
    {
       var dateTime = (DateTime)target;

       // Note: do not dispose the writer so the outgoingData remains open.
       // If targeting newer frameworks you can use the leaveOpen parameter,too.
       var writer = new BinaryWriter(outgoingData);

       // What a tiny payload compared to the default BinaryFormatter result...
       writer.Write(dateTime.Ticks);
       writer.Write((int)dateTime.Kind);
    }
}

在可视化工具本身中,重要的是不要调用 GetObject,它会尝试通过 BinaryFormatter 反序列化您的流。相反,使用 GetData,它返回原始流:

internal class DateTimeDebuggerVisualizer : DialogDebuggerVisualizer
{
    protected override void Show(IDialogVisualizerService windowService,IVisualizerObjectProvider objectProvider)
    {
        // GetObject would fail here as we have a custom written stream
        var reader = new BinaryReader(objectProvider.GetData());

        var dateTime = new DateTime(reader.ReadInt64(),(DateTimeKind)reader.ReadInt32());

        // Show the debugger [...]
    }

如果您的调试器可以编辑数据,那么替换值是另一个问题。不幸的是,在这种情况下无法避免 BinaryFormatter 序列化。如果您的对象不是在每个目标中都可序列化(例如在 .NET Core 及更高版本中),则可能会出现问题。

当然,这在 DateTime 的情况下不是问题,所以作为旁注:在这种情况下,技巧可以是将自定义的可序列化数据放入 ReplaceObject 中实现 {{ 1}} 以便您可以在 IObjectReference 中返回自定义结果。 Here 就是我的序列化程序中的一个这样的例子。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。