diff --git a/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/EgressHelper.cs b/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/EgressHelper.cs index 885de11855f..ca6750c747f 100644 --- a/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/EgressHelper.cs +++ b/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/EgressHelper.cs @@ -7,8 +7,10 @@ using System.Collections.Generic; using System.CommandLine; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; @@ -20,6 +22,7 @@ internal sealed class EgressHelper { private static Stream StdInStream; private static CancellationTokenSource CancelSource = new CancellationTokenSource(); + private const int ExpectedPayloadProtocolVersion = 1; internal static CliCommand CreateEgressCommand(EgressProvider provider, Action configureOptions = null) where TOptions : class, new() { @@ -35,8 +38,32 @@ internal sealed class EgressHelper EgressArtifactResult result = new(); try { - string jsonConfig = Console.ReadLine(); - ExtensionEgressPayload configPayload = JsonSerializer.Deserialize(jsonConfig); + StdInStream = Console.OpenStandardInput(); + + int dotnetMonitorPayloadProtocolVersion; + long payloadLengthBuffer; + byte[] payloadBuffer; + + using (BinaryReader reader = new BinaryReader(StdInStream, Encoding.UTF8, leaveOpen: true)) + { + dotnetMonitorPayloadProtocolVersion = reader.ReadInt32(); + if (dotnetMonitorPayloadProtocolVersion != ExpectedPayloadProtocolVersion) + { + throw new EgressException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorMessage_IncorrectPayloadVersion, dotnetMonitorPayloadProtocolVersion, ExpectedPayloadProtocolVersion)); + } + + payloadLengthBuffer = reader.ReadInt64(); + + if (payloadLengthBuffer < 0) + { + throw new ArgumentOutOfRangeException(nameof(payloadLengthBuffer)); + } + } + + payloadBuffer = new byte[payloadLengthBuffer]; + await ReadExactlyAsync(payloadBuffer, token); + + ExtensionEgressPayload configPayload = JsonSerializer.Deserialize(payloadBuffer); using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => { @@ -113,9 +140,27 @@ private static async Task GetStream(Stream outputStream, CancellationToken cance { const int DefaultBufferSize = 0x10000; - StdInStream = Console.OpenStandardInput(); await StdInStream.CopyToAsync(outputStream, DefaultBufferSize, cancellationToken); } + + private static async Task ReadExactlyAsync(Memory buffer, CancellationToken token) + { +#if NET7_0_OR_GREATER + await StdInStream.ReadExactlyAsync(buffer, token); +#else + int totalRead = 0; + while (totalRead < buffer.Length) + { + int read = await StdInStream.ReadAsync(buffer.Slice(totalRead), token).ConfigureAwait(false); + if (read == 0) + { + throw new EndOfStreamException(); + } + + totalRead += read; + } +#endif + } } internal sealed class ExtensionEgressPayload diff --git a/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/Microsoft.Diagnostics.Monitoring.Extension.Common.csproj b/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/Microsoft.Diagnostics.Monitoring.Extension.Common.csproj index a601f2f365f..4f8e827857f 100644 --- a/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/Microsoft.Diagnostics.Monitoring.Extension.Common.csproj +++ b/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/Microsoft.Diagnostics.Monitoring.Extension.Common.csproj @@ -22,4 +22,20 @@ + + + + Strings.resx + True + True + + + + + + Designer + Strings.Designer.cs + ResXFileCodeGenerator + + diff --git a/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/Strings.Designer.cs b/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/Strings.Designer.cs new file mode 100644 index 00000000000..afde1de1206 --- /dev/null +++ b/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/Strings.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Diagnostics.Monitoring.Extension.Common { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Diagnostics.Monitoring.Extension.Common.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The egress operation failed due to an internal error.. + /// + internal static string ErrorMessage_GenericEgressFailure { + get { + return ResourceManager.GetString("ErrorMessage_GenericEgressFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unsupported egress extension protocol. Please ensure both dotnet-monitor and this extension are up-to-date. Dotnet-Monitor Version: v{0}; Extension Version: v{1}. + /// + internal static string ErrorMessage_IncorrectPayloadVersion { + get { + return ResourceManager.GetString("ErrorMessage_IncorrectPayloadVersion", resourceCulture); + } + } + } +} diff --git a/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/Strings.resx b/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/Strings.resx new file mode 100644 index 00000000000..ab751ec191a --- /dev/null +++ b/src/Extensions/Microsoft.Diagnostics.Monitoring.Extension.Common/Strings.resx @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The egress operation failed due to an internal error. + + + Unsupported egress extension protocol. Please ensure both dotnet-monitor and this extension are up-to-date. Dotnet-Monitor Version: v{0}; Extension Version: v{1} + Gets the format string for failing an egress operation due to using the incorrect payload protocol version +2 Format Parameters: +0. extensionVersion: The extension's version +1. payloadVersion: The payload's version + + \ No newline at end of file diff --git a/src/Tools/dotnet-monitor/Egress/Extension/EgressExtension.cs b/src/Tools/dotnet-monitor/Egress/Extension/EgressExtension.cs index 14808703c56..5eb2e03dc98 100644 --- a/src/Tools/dotnet-monitor/Egress/Extension/EgressExtension.cs +++ b/src/Tools/dotnet-monitor/Egress/Extension/EgressExtension.cs @@ -12,6 +12,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -26,6 +27,7 @@ internal partial class EgressExtension : IExtension, IEgressExtension private readonly ILogger _logger; private readonly ExtensionManifest _manifest; private readonly IDictionary _processEnvironmentVariables = new Dictionary(); + private const int PayloadProtocolVersion = 1; private static readonly TimeSpan WaitForProcessExitTimeout = TimeSpan.FromMilliseconds(2000); @@ -75,9 +77,6 @@ public async Task EgressArtifact( { _manifest.Validate(); - // This is really weird, yes, but this is one of 2 overloads for [Stream].WriteAsync(...) that supports a CancellationToken, so we use a ReadOnlyMemory instead of a string. - ReadOnlyMemory NewLine = new ReadOnlyMemory("\r\n".ToCharArray()); - ProcessStartInfo pStart = new ProcessStartInfo() { RedirectStandardInput = true, @@ -148,9 +147,23 @@ public async Task EgressArtifact( parser.BeginReading(); - await JsonSerializer.SerializeAsync(p.StandardInput.BaseStream, payload, options: null, token); - await p.StandardInput.WriteAsync(NewLine, token); + // p.StandardInput.BaseStream Format: Version (int), Payload Length (long), Payload, Artifact + using Stream intermediateStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(intermediateStream, payload, options: null, token); + + using (BinaryWriter writer = new BinaryWriter(p.StandardInput.BaseStream, Encoding.UTF8, leaveOpen: true)) + { + writer.Write(PayloadProtocolVersion); + writer.Write(intermediateStream.Position); + + intermediateStream.Position = 0; + + writer.Flush(); + } + + await intermediateStream.CopyToAsync(p.StandardInput.BaseStream, token); await p.StandardInput.BaseStream.FlushAsync(token); + _logger.ExtensionConfigured(pStart.FileName, p.Id); await action(p.StandardInput.BaseStream, token);