diff --git a/MemoryStreamDebugVisualizer/DebugVisualizer.slnx b/MemoryStreamDebugVisualizer/DebugVisualizer.slnx new file mode 100644 index 0000000..2ac69b0 --- /dev/null +++ b/MemoryStreamDebugVisualizer/DebugVisualizer.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/MemoryStreamDebugVisualizer/Directory.Build.props b/MemoryStreamDebugVisualizer/Directory.Build.props new file mode 100644 index 0000000..9a6f2ef --- /dev/null +++ b/MemoryStreamDebugVisualizer/Directory.Build.props @@ -0,0 +1,14 @@ + + + Debug + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)')) + $(RepoRootPath)obj\$(MSBuildProjectName)\ + $(RepoRootPath)bin\$(MSBuildProjectName)\ + false + false + true + false + false + latest + + diff --git a/MemoryStreamDebugVisualizer/MemoryStreamObjectSource/MemoryStreamObjectSource.csproj b/MemoryStreamDebugVisualizer/MemoryStreamObjectSource/MemoryStreamObjectSource.csproj new file mode 100644 index 0000000..0350e38 --- /dev/null +++ b/MemoryStreamDebugVisualizer/MemoryStreamObjectSource/MemoryStreamObjectSource.csproj @@ -0,0 +1,14 @@ + + + netstandard2.0 + enable + 12 + + + + + + + + + diff --git a/MemoryStreamDebugVisualizer/MemoryStreamObjectSource/MemoryStreamVisualizerObjectSource.cs b/MemoryStreamDebugVisualizer/MemoryStreamObjectSource/MemoryStreamVisualizerObjectSource.cs new file mode 100644 index 0000000..60656a9 --- /dev/null +++ b/MemoryStreamDebugVisualizer/MemoryStreamObjectSource/MemoryStreamVisualizerObjectSource.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +namespace MemoryStreamObjectSource; + +using Microsoft.VisualStudio.DebuggerVisualizers; +using System.IO; + +/// +/// Object source class for the MemoryStreamVisualizer. +/// +public class MemoryStreamVisualizerObjectSource : VisualizerObjectSource +{ + /// + /// How many rows will be transfered, at most, responding to a single request. + /// + public const int RowsCountPerRequest = 1024; + + /// + /// How many bytes will be transfered for each row. + /// + public const int RowLength = 16; + + /// + public override void TransferData(object target, Stream incomingData, Stream outgoingData) + { + if (target is not MemoryStream memoryStream) + { + return; + } + + + using BinaryReader binaryReader = new(incomingData); + var index = binaryReader.ReadInt32(); // The extension will send the offset (Int32) to start reading from + + using BinaryWriter binaryWriter = new(outgoingData); + var backupPosition = memoryStream.Position; + + // Will reply with the current MemoryStream.Position (Int64), + // followed by MemoryStream.Length (Int64), + // followed by up to 16KB of data retrieved from the MemoryStream + binaryWriter.Write(backupPosition); + binaryWriter.Write(memoryStream.Length); + if (index < memoryStream.Length) + { + try + { + var data = new byte[RowsCountPerRequest * RowLength]; + memoryStream.Seek(index, SeekOrigin.Begin); + var count = memoryStream.Read(data, 0, data.Length); + binaryWriter.Write(data, 0, count); + } + finally + { + // Make sure to restore the MemoryStream to its original position + memoryStream.Seek(backupPosition, SeekOrigin.Begin); + } + } + + binaryWriter.Flush(); + + } +} diff --git a/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/ExtensionEntrypoint.cs b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/ExtensionEntrypoint.cs new file mode 100644 index 0000000..7119fb1 --- /dev/null +++ b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/ExtensionEntrypoint.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MemoryStreamVisualizer; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.Extensibility; + +/// +/// Extension entrypoint for the VisualStudio.Extensibility extension. +/// +[VisualStudioContribution] +internal class ExtensionEntrypoint : Extension +{ + /// + public override ExtensionConfiguration ExtensionConfiguration => new() + { + Metadata = new( + id: "MemoryStreamVisualizer.97a0a2fb-f163-4fa3-91f0-48a2d4ad9f57", + version: this.ExtensionAssemblyVersion, + publisherName: "Microsoft", + displayName: "MemoryStream Debugger Visualizer", + description: "A debugger visualizer for MemoryStream"), + }; + + /// + protected override void InitializeServices(IServiceCollection serviceCollection) + { + base.InitializeServices(serviceCollection); + + // You can configure dependency injection here by adding services to the serviceCollection. + } +} diff --git a/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/HexEditorRow.cs b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/HexEditorRow.cs new file mode 100644 index 0000000..8d78106 --- /dev/null +++ b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/HexEditorRow.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MemoryStreamVisualizer; + +using System.Runtime.Serialization; + +/// +/// ViewModel class representing a row of binary data. +/// +[DataContract] +public class HexEditorRow +{ + /// + /// Initializes a new instance of the class. + /// + /// The index of this row. + /// The bytes making up this row of data, in their hex representation. + /// The bytes making up this row of data, in their Ascii representation. + public HexEditorRow(int index, string data, string ascii) + { + this.Index = index; + this.Data = data; + this.Ascii = ascii; + } + + /// + /// Gets the index of this row. + /// + [DataMember] + public int Index { get; } + + /// + /// Gets the index of this row in hex format. + /// + [DataMember] + public string HexIndex => $"{this.Index:X8}h"; + + /// + /// Gets the bytes making up this row of data, in their hex representation. + /// + [DataMember] + public string Data { get; } + + /// + /// Gets the bytes making up this row of data, in their Ascii representation. + /// + [DataMember] + public string Ascii { get; } +} diff --git a/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamData.cs b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamData.cs new file mode 100644 index 0000000..fa7d396 --- /dev/null +++ b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamData.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MemoryStreamVisualizer; + +using System.IO; +using System.Runtime.Serialization; +using System.Windows; +using Microsoft.VisualStudio.Extensibility.UI; + +/// +/// ViewModel class representing the data contained by a . +/// +[DataContract] +public class MemoryStreamData : NotifyPropertyChangedObject +{ + private long length; + private long position; + private Visibility loadingVisibility = Visibility.Visible; + + /// + /// Gets or sets the length of the . + /// + [DataMember] + public long Length + { + get => this.length; + set + { + if (this.SetProperty(ref this.length, value)) + { + this.RaiseNotifyPropertyChangedEvent(nameof(this.HexLength)); + } + } + } + + /// + /// Gets the length of the in hex format. + /// + [DataMember] + public string HexLength => $"{this.length:X}h"; + + /// + /// Gets or sets the current position of the . + /// + [DataMember] + public long Position + { + get => this.position; + set + { + if (this.SetProperty(ref this.position, value)) + { + this.RaiseNotifyPropertyChangedEvent(nameof(this.HexPosition)); + } + } + } + + /// + /// Gets the current position of the in hex format. + /// + [DataMember] + public string HexPosition => $"{this.position:X}h"; + + /// + /// Gets the data currently contained in the . + /// + [DataMember] + public ObservableList Data { get; } = new(); + + /// + /// Gets or sets whether the loading bar should be visible. + /// + [DataMember] + public Visibility LoadingVisibility + { + get => this.loadingVisibility; + set => this.SetProperty(ref this.loadingVisibility, value); + } +} diff --git a/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamDebuggerVisualizerProvider.cs b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamDebuggerVisualizerProvider.cs new file mode 100644 index 0000000..f614e63 --- /dev/null +++ b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamDebuggerVisualizerProvider.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MemoryStreamVisualizer; + +using MemoryStreamObjectSource; +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers; +using Microsoft.VisualStudio.RpcContracts.RemoteUI; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +/// +/// Debugger visualizer provider for . +/// +[VisualStudioContribution] +internal class MemoryStreamDebuggerVisualizerProvider : DebuggerVisualizerProvider +{ + /// + public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new( + [ + new VisualizerTargetType("%MemoryStreamVisualizer.MemoryStreamDebuggerVisualizerProvider.Name%", typeof(MemoryStream)) + ]) + { + VisualizerObjectSourceType = new VisualizerObjectSourceType(typeof(MemoryStreamVisualizerObjectSource)), + Style = VisualizerStyle.ToolWindow, + }; + + /// + public override Task CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken) + { + return Task.FromResult(new MemoryStreamVisualizerUserControl(visualizerTarget)); + } +} diff --git a/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamVisualizer.csproj b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamVisualizer.csproj new file mode 100644 index 0000000..2024e28 --- /dev/null +++ b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamVisualizer.csproj @@ -0,0 +1,29 @@ + + + net8.0-windows8.0 + enable + 12 + en-US + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamVisualizerUserControl.cs b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamVisualizerUserControl.cs new file mode 100644 index 0000000..bdf3e66 --- /dev/null +++ b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamVisualizerUserControl.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + + +using MemoryStreamObjectSource; +using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers; +using Microsoft.VisualStudio.Extensibility.UI; +using Microsoft.VisualStudio.RpcContracts.DebuggerVisualizers; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; + +namespace MemoryStreamVisualizer; +/// +/// Remote UI user control for the MemoryStreamVisualizer. +/// +internal class MemoryStreamVisualizerUserControl : RemoteUserControl +{ + private readonly VisualizerTarget visualizerTarget; + private readonly MemoryStreamData dataContext; + + /// + /// Initializes a new instance of the class. + /// + /// The visualizer target to be used to retrieve data from. + public MemoryStreamVisualizerUserControl(VisualizerTarget visualizerTarget) + : base(new MemoryStreamData()) + { + visualizerTarget.Changed += this.VisualizerTargetStateChangedAsync; + + this.dataContext = (MemoryStreamData)this.DataContext!; + this.visualizerTarget = visualizerTarget; + } + + private Task VisualizerTargetStateChangedAsync(VisualizerTargetStateNotification args) + { + if (args == VisualizerTargetStateNotification.Available || args == VisualizerTargetStateNotification.ValueUpdated) + { + this.dataContext.Data.Clear(); + this.dataContext.Position = 0; + this.dataContext.Length = 0; + this.dataContext.LoadingVisibility = Visibility.Visible; + + return this.RetrieveDataAsync(); + } + + return Task.CompletedTask; + } + + private async Task RetrieveDataAsync() + { + ReadOnlySequence data; + do + { + using MemoryStream memoryStream = new(sizeof(int)); + using BinaryWriter binaryWriter = new(memoryStream); + int index = this.dataContext.Data.Count * MemoryStreamVisualizerObjectSource.RowLength; + if (index >= 1024 * 1024) + { + break; // Let's not retrieve more than 1MB of data + } + + binaryWriter.Write(index); + binaryWriter.Flush(); + try + { + data = (await this.visualizerTarget.ObjectSource.RequestDataAsync(new ReadOnlySequence(memoryStream.ToArray()), CancellationToken.None)).Value; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception) +#pragma warning restore CA1031 // Do not catch general exception types + { + // I can get an exception if the debug session is unpaused, so I need to handle it gracefully + break; + } + } + while (data.Length > 0 && this.Read(data)); + + this.dataContext.LoadingVisibility = Visibility.Hidden; + } + + private bool Read(ReadOnlySequence data) + { + int byteInRowCount = 0; + StringBuilder binaryText = new(); + StringBuilder asciiText = new(); + + SequenceReader reader = new(data); + if (!reader.TryReadLittleEndian(out long position) || !reader.TryReadLittleEndian(out long length)) + { + return false; + } + + this.dataContext.Position = position; + this.dataContext.Length = length; + + if (reader.UnreadSpan.Length == 0) + { + return false; // We always receive data unless we are at the end of the MemoryStream + } + + List rows = new(MemoryStreamVisualizerObjectSource.RowsCountPerRequest); + byte[] tmp = new byte[1]; + while (reader.TryRead(out byte b)) + { + byteInRowCount++; + if (byteInRowCount > 1) + { + binaryText.Append(' '); + } + + binaryText.Append(b.ToString("X2", CultureInfo.InvariantCulture)); + tmp[0] = b; + asciiText.Append(char.IsControl((char)b) || b == 0xAD ? '•' : Encoding.Latin1.GetChars(tmp)[0]); + + if (byteInRowCount == MemoryStreamVisualizerObjectSource.RowLength) + { + CompleteRow(); + } + } + + if (byteInRowCount > 0) + { + CompleteRow(); + this.dataContext.Data.AddRange(rows); + return false; // We only receive partial rows at the end of the MemoryStream + } + + this.dataContext.Data.AddRange(rows); + return true; + + void CompleteRow() + { + rows.Add(new HexEditorRow( + index: (this.dataContext.Data.Count + rows.Count) * MemoryStreamVisualizerObjectSource.RowLength, + data: binaryText.ToString(), + ascii: asciiText.ToString())); + + byteInRowCount = 0; + binaryText.Clear(); + asciiText.Clear(); + } + } +} diff --git a/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamVisualizerUserControl.xaml b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamVisualizerUserControl.xaml new file mode 100644 index 0000000..2c6b164 --- /dev/null +++ b/MemoryStreamDebugVisualizer/MemoryStreamVisualizer/MemoryStreamVisualizerUserControl.xaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + Hex + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MemoryStreamDebugVisualizer/TestApp/Program.cs b/MemoryStreamDebugVisualizer/TestApp/Program.cs new file mode 100644 index 0000000..d93c1e0 --- /dev/null +++ b/MemoryStreamDebugVisualizer/TestApp/Program.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) PlaceholderCompany. All rights reserved. +// + +using System.Text; + +// Initialize a MemoryStream to store user input temporarily +using MemoryStream userInputStream = new(); + +// Convert user input string to byte array +string userInput = "Sample user input data"; + +byte[] inputData = Encoding.UTF8.GetBytes(userInput); + +// Write user input data to the MemoryStream +userInputStream.Write(inputData, 0, inputData.Length); +var buffer = new byte[inputData.Length]; +userInputStream.Position = 0; +userInputStream.Read(buffer); +var text = Encoding.UTF8.GetString(buffer, 0, buffer.Length); + +// Perform further processing with the stored user input +Thread.Sleep(1000); \ No newline at end of file diff --git a/MemoryStreamDebugVisualizer/TestApp/TestApp.csproj b/MemoryStreamDebugVisualizer/TestApp/TestApp.csproj new file mode 100644 index 0000000..489df93 --- /dev/null +++ b/MemoryStreamDebugVisualizer/TestApp/TestApp.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + enable + + + + + + +