From dfb97e8c4561f5ef40b7853faa6f1ca9b5d8f8c9 Mon Sep 17 00:00:00 2001 From: David Mason Date: Mon, 28 Jun 2021 11:17:35 -0700 Subject: [PATCH] Add new DiagnosticClient commands for IPC features (#2268) Client commands for dotnet/runtime#52175 and dotnet/runtime#52567 --- ...diagnostics-client-library-instructions.md | 26 ++- .../DiagnosticsClient/DiagnosticsClient.cs | 212 ++++++++++++++---- .../DiagnosticsIpc/IpcCommands.cs | 2 + .../DiagnosticsIpc/IpcMessage.cs | 3 +- .../DiagnosticsIpc/ProcessEnvironment.cs | 14 +- .../DiagnosticsIpc/ProcessInfo.cs | 19 +- .../IpcHelpers.cs | 22 ++ .../TestRunner.cs | 5 +- 8 files changed, 225 insertions(+), 78 deletions(-) create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/IpcHelpers.cs diff --git a/documentation/diagnostics-client-library-instructions.md b/documentation/diagnostics-client-library-instructions.md index 09fcfdd71..1ebddb12b 100644 --- a/documentation/diagnostics-client-library-instructions.md +++ b/documentation/diagnostics-client-library-instructions.md @@ -224,14 +224,36 @@ public static void PrintEventsLive(int processId) This sample shows how to attach an ICorProfiler to a process (profiler attach). ```cs -public static int AttachProfiler(int processId, Guid profilerGuid, string profilerPath) +public static void AttachProfiler(int processId, Guid profilerGuid, string profilerPath) { var client = new DiagnosticsClient(processId); - return client.AttachProfiler(TimeSpan.FromSeconds(10), profilerGuid, profilerPath); + client.AttachProfiler(TimeSpan.FromSeconds(10), profilerGuid, profilerPath); } ``` +#### 8. Set an ICorProfiler to be used as the startup profiler +This sample shows how to request that the runtime use an ICorProfiler as the startup profiler (not as an attaching profiler). It is only valid to issue this command while the runtime is paused in "reverse server" mode. + +```cs +public static void SetStartupProfilerProfiler(Guid profilerGuid, string profilerPath) +{ + var client = new DiagnosticsClient(processId); + client.SetStartupProfiler(profilerGuid, profilerPath); +} +``` + +#### 9. Resume the runtime when it is paused in reverse server mode + +This sample shows how a client can instruct the runtime to resume loading after it has been paused in "reverse server" mode. + +```cs +public static void ResumeRuntime(Guid profilerGuid, string profilerPath) +{ + var client = new DiagnosticsClient(processId); + client.ResumeRuntime(); +} +``` ## API Description diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs index 6b11c7507..0cdc5c14b 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs @@ -61,7 +61,7 @@ namespace Microsoft.Diagnostics.NETCore.Client /// /// An EventPipeSession object representing the EventPipe session that just started. /// - public EventPipeSession StartEventPipeSession(IEnumerable providers, bool requestRundown=true, int circularBufferMB=256) + public EventPipeSession StartEventPipeSession(IEnumerable providers, bool requestRundown = true, int circularBufferMB = 256) { return new EventPipeSession(_endpoint, providers, requestRundown, circularBufferMB); } @@ -75,7 +75,7 @@ namespace Microsoft.Diagnostics.NETCore.Client /// /// An EventPipeSession object representing the EventPipe session that just started. /// - public EventPipeSession StartEventPipeSession(EventPipeProvider provider, bool requestRundown=true, int circularBufferMB=256) + public EventPipeSession StartEventPipeSession(EventPipeProvider provider, bool requestRundown = true, int circularBufferMB = 256) { return new EventPipeSession(_endpoint, new[] { provider }, requestRundown, circularBufferMB); } @@ -86,12 +86,12 @@ namespace Microsoft.Diagnostics.NETCore.Client /// Type of the dump to be generated /// Full path to the dump to be generated. By default it is /tmp/coredump.{pid} /// When set to true, display the dump generation debug log to the console. - public void WriteDump(DumpType dumpType, string dumpPath, bool logDumpGeneration=false) + public void WriteDump(DumpType dumpType, string dumpPath, bool logDumpGeneration = false) { if (string.IsNullOrEmpty(dumpPath)) throw new ArgumentNullException($"{nameof(dumpPath)} required"); - byte[] payload = SerializeCoreDump(dumpPath, dumpType, logDumpGeneration); + byte[] payload = SerializePayload(dumpPath, (uint)dumpType, logDumpGeneration); IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Dump, (byte)DumpCommandId.GenerateCoreDump, payload); IpcMessage response = IpcClient.SendMessage(_endpoint, message); switch ((DiagnosticsServerResponseId)response.Header.CommandId) @@ -117,7 +117,7 @@ namespace Microsoft.Diagnostics.NETCore.Client /// Guid for the profiler to be attached /// Path to the profiler to be attached /// Additional data to be passed to the profiler - public void AttachProfiler(TimeSpan attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData=null) + public void AttachProfiler(TimeSpan attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData = null) { if (profilerGuid == null || profilerGuid == Guid.Empty) { @@ -129,7 +129,7 @@ namespace Microsoft.Diagnostics.NETCore.Client throw new ArgumentException($"{nameof(profilerPath)} must be non-null"); } - byte[] serializedConfiguration = SerializeProfilerAttach((uint)attachTimeout.TotalSeconds, profilerGuid, profilerPath, additionalData); + byte[] serializedConfiguration = SerializePayload((uint)attachTimeout.TotalSeconds, profilerGuid, profilerPath, additionalData); var message = new IpcMessage(DiagnosticsServerCommandSet.Profiler, (byte)ProfilerCommandId.AttachProfiler, serializedConfiguration); var response = IpcClient.SendMessage(_endpoint, message); switch ((DiagnosticsServerResponseId)response.Header.CommandId) @@ -138,7 +138,7 @@ namespace Microsoft.Diagnostics.NETCore.Client uint hr = BitConverter.ToUInt32(response.Payload, 0); if (hr == (uint)DiagnosticsIpcError.UnknownCommand) { - throw new UnsupportedCommandException("The target runtime does not support profiler attach"); + throw new UnsupportedCommandException("The target runtime does not support profiler attach"); } if (hr == (uint)DiagnosticsIpcError.ProfilerAlreadyActive) { @@ -156,35 +156,61 @@ namespace Microsoft.Diagnostics.NETCore.Client // runtime timeout or respect attachTimeout as one total duration. } - internal void ResumeRuntime() + /// + /// Set a profiler as the startup profiler. It is only valid to issue this command + /// while the runtime is paused in the "reverse server" mode. + /// + /// Guid for the profiler to be attached + /// Path to the profiler to be attached + public void SetStartupProfiler(Guid profilerGuid, string profilerPath) { - IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.ResumeRuntime); + if (profilerGuid == null || profilerGuid == Guid.Empty) + { + throw new ArgumentException($"{nameof(profilerGuid)} must be a valid Guid"); + } + + if (String.IsNullOrEmpty(profilerPath)) + { + throw new ArgumentException($"{nameof(profilerPath)} must be non-null"); + } + + byte[] serializedConfiguration = SerializePayload(profilerGuid, profilerPath); + var message = new IpcMessage(DiagnosticsServerCommandSet.Profiler, (byte)ProfilerCommandId.StartupProfiler, serializedConfiguration); var response = IpcClient.SendMessage(_endpoint, message); switch ((DiagnosticsServerResponseId)response.Header.CommandId) { case DiagnosticsServerResponseId.Error: - // Try fallback for Preview 7 and Preview 8 - ResumeRuntimeFallback(); - //var hr = BitConverter.ToInt32(response.Payload, 0); - //throw new ServerErrorException($"Resume runtime failed (HRESULT: 0x{hr:X8})"); - return; + uint hr = BitConverter.ToUInt32(response.Payload, 0); + if (hr == (uint)DiagnosticsIpcError.UnknownCommand) + { + throw new UnsupportedCommandException("The target runtime does not support the ProfilerStartup command."); + } + else if (hr == (uint)DiagnosticsIpcError.InvalidArgument) + { + throw new ServerErrorException("The runtime must be suspended to issue the SetStartupProfiler command."); + } + + throw new ServerErrorException($"Profiler startup failed (HRESULT: 0x{hr:X8})"); case DiagnosticsServerResponseId.OK: return; default: - throw new ServerErrorException($"Resume runtime failed - server responded with unknown command"); + throw new ServerErrorException($"Profiler startup failed - server responded with unknown command"); } } - // Fallback command for .NET 5 Preview 7 and Preview 8 - internal void ResumeRuntimeFallback() + /// + /// Tell the runtime to resume execution after being paused for "reverse server" mode. + /// + public void ResumeRuntime() { - IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Server, (byte)DiagnosticServerCommandId.ResumeRuntime); + IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.ResumeRuntime); var response = IpcClient.SendMessage(_endpoint, message); switch ((DiagnosticsServerResponseId)response.Header.CommandId) { case DiagnosticsServerResponseId.Error: - var hr = BitConverter.ToInt32(response.Payload, 0); - throw new ServerErrorException($"Resume runtime failed (HRESULT: 0x{hr:X8})"); + // Try fallback for Preview 7 and Preview 8 + ResumeRuntimeFallback(); + return; case DiagnosticsServerResponseId.OK: return; default: @@ -192,23 +218,43 @@ namespace Microsoft.Diagnostics.NETCore.Client } } - internal ProcessInfo GetProcessInfo() + /// + /// Set an environment variable in the target process. + /// + /// The name of the environment variable to set. + /// The value of the environment variable to set. + public void SetEnvironmentVariable(string name, string value) { - IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessInfo); + if (String.IsNullOrEmpty(name)) + { + throw new ArgumentException($"{nameof(name)} must be non-null."); + } + + byte[] serializedConfiguration = SerializePayload(name, value); + var message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.SetEnvironmentVariable, serializedConfiguration); var response = IpcClient.SendMessage(_endpoint, message); switch ((DiagnosticsServerResponseId)response.Header.CommandId) { case DiagnosticsServerResponseId.Error: - var hr = BitConverter.ToInt32(response.Payload, 0); - throw new ServerErrorException($"Get process info failed (HRESULT: 0x{hr:X8})"); + uint hr = BitConverter.ToUInt32(response.Payload, 0); + if (hr == (uint)DiagnosticsIpcError.UnknownCommand) + { + throw new UnsupportedCommandException("The target runtime does not support the SetEnvironmentVariable command."); + } + + throw new ServerErrorException($"SetEnvironmentVariable failed (HRESULT: 0x{hr:X8})"); case DiagnosticsServerResponseId.OK: - return ProcessInfo.Parse(response.Payload); + return; default: - throw new ServerErrorException($"Get process info failed - server responded with unknown command"); + throw new ServerErrorException($"SetEnvironmentVariable failed - server responded with unknown command"); } } - public Dictionary GetProcessEnvironment() + /// + /// Gets all environement variables and their values from the target process. + /// + /// A dictionary containing all of the environment variables defined in the target process. + public Dictionary GetProcessEnvironment() { var message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessEnvironment); Stream continuation = IpcClient.SendMessage(_endpoint, message, out IpcMessage response); @@ -219,7 +265,7 @@ namespace Microsoft.Diagnostics.NETCore.Client throw new ServerErrorException($"Get process environment failed (HRESULT: 0x{hr:X8})"); case DiagnosticsServerResponseId.OK: ProcessEnvironmentHelper helper = ProcessEnvironmentHelper.Parse(response.Payload); - Task> envTask = helper.ReadEnvironmentAsync(continuation); + Task> envTask = helper.ReadEnvironmentAsync(continuation); envTask.Wait(); return envTask.Result; default: @@ -253,42 +299,118 @@ namespace Microsoft.Diagnostics.NETCore.Client return GetAllPublishedProcesses().Distinct(); } - private static byte[] SerializeCoreDump(string dumpName, DumpType dumpType, bool diagnostics) + + // Fallback command for .NET 5 Preview 7 and Preview 8 + internal void ResumeRuntimeFallback() + { + IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Server, (byte)DiagnosticServerCommandId.ResumeRuntime); + var response = IpcClient.SendMessage(_endpoint, message); + switch ((DiagnosticsServerResponseId)response.Header.CommandId) + { + case DiagnosticsServerResponseId.Error: + var hr = BitConverter.ToInt32(response.Payload, 0); + throw new ServerErrorException($"Resume runtime failed (HRESULT: 0x{hr:X8})"); + case DiagnosticsServerResponseId.OK: + return; + default: + throw new ServerErrorException($"Resume runtime failed - server responded with unknown command"); + } + } + + internal ProcessInfo GetProcessInfo() + { + IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessInfo); + var response = IpcClient.SendMessage(_endpoint, message); + switch ((DiagnosticsServerResponseId)response.Header.CommandId) + { + case DiagnosticsServerResponseId.Error: + var hr = BitConverter.ToInt32(response.Payload, 0); + throw new ServerErrorException($"Get process info failed (HRESULT: 0x{hr:X8})"); + case DiagnosticsServerResponseId.OK: + return ProcessInfo.Parse(response.Payload); + default: + throw new ServerErrorException($"Get process info failed - server responded with unknown command"); + } + } + + private static byte[] SerializePayload(T arg) { using (var stream = new MemoryStream()) using (var writer = new BinaryWriter(stream)) { - writer.WriteString(dumpName); - writer.Write((uint)dumpType); - writer.Write((uint)(diagnostics ? 1 : 0)); + SerializePayloadArgument(arg, writer); writer.Flush(); return stream.ToArray(); } } - private static byte[] SerializeProfilerAttach(uint attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData) + private static byte[] SerializePayload(T1 arg1, T2 arg2) { using (var stream = new MemoryStream()) using (var writer = new BinaryWriter(stream)) { - writer.Write(attachTimeout); - writer.Write(profilerGuid.ToByteArray()); - writer.WriteString(profilerPath); + SerializePayloadArgument(arg1, writer); + SerializePayloadArgument(arg2, writer); - if (additionalData == null) - { - writer.Write(0); - } - else - { - writer.Write(additionalData.Length); - writer.Write(additionalData); - } + writer.Flush(); + return stream.ToArray(); + } + } + + private static byte[] SerializePayload(T1 arg1, T2 arg2, T3 arg3) + { + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + { + SerializePayloadArgument(arg1, writer); + SerializePayloadArgument(arg2, writer); + SerializePayloadArgument(arg3, writer); writer.Flush(); return stream.ToArray(); } } + + private static byte[] SerializePayload(T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + { + SerializePayloadArgument(arg1, writer); + SerializePayloadArgument(arg2, writer); + SerializePayloadArgument(arg3, writer); + SerializePayloadArgument(arg4, writer); + + writer.Flush(); + return stream.ToArray(); + } + } + + private static void SerializePayloadArgument(T obj, BinaryWriter writer) + { + if (typeof(T) == typeof(string)) + { + writer.WriteString((string)((object)obj)); + } + else if (typeof(T) == typeof(int)) + { + writer.Write((int)((object)obj)); + } + else if (typeof(T) == typeof(uint)) + { + writer.Write((uint)((object)obj)); + } + else if (typeof(T) == typeof(bool)) + { + bool bValue = (bool)((object)obj); + uint uiValue = bValue ? 1 : 0; + writer.Write(uiValue); + } + else + { + throw new ArgumentException($"Type {obj.GetType()} is not supported in SerializePayloadArgument, please add it."); + } + } } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs index ca59ba255..68442de6c 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs @@ -51,6 +51,7 @@ namespace Microsoft.Diagnostics.NETCore.Client internal enum ProfilerCommandId : byte { AttachProfiler = 0x01, + StartupProfiler = 0x02, } internal enum ProcessCommandId : byte @@ -58,5 +59,6 @@ namespace Microsoft.Diagnostics.NETCore.Client GetProcessInfo = 0x00, ResumeRuntime = 0x01, GetProcessEnvironment = 0x02, + SetEnvironmentVariable = 0x03, } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs index 372d49ef3..00b50980a 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs @@ -15,11 +15,12 @@ namespace Microsoft.Diagnostics.NETCore.Client /// internal enum DiagnosticsIpcError : uint { + InvalidArgument = 0x80070057, ProfilerAlreadyActive = 0x8013136A, BadEncoding = 0x80131384, UnknownCommand = 0x80131385, UnknownMagic = 0x80131386, - UnknownError = 0x80131387 + UnknownError = 0x80131387, } /// diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs index 318e1d96f..b3985e49c 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs @@ -42,7 +42,7 @@ namespace Microsoft.Diagnostics.NETCore.Client cursor += sizeof(UInt32); while (cursor < envBlock.Length) { - string pair = ReadString(envBlock, ref cursor); + string pair = IpcHelpers.ReadString(envBlock, ref cursor); int equalsIdx = pair.IndexOf('='); env[pair.Substring(0,equalsIdx)] = equalsIdx != pair.Length - 1 ? pair.Substring(equalsIdx+1) : ""; } @@ -50,18 +50,6 @@ namespace Microsoft.Diagnostics.NETCore.Client return env; } - private static string ReadString(byte[] buffer, ref int index) - { - // Length of the string of UTF-16 characters - int length = (int)BitConverter.ToUInt32(buffer, index); - index += sizeof(UInt32); - - int size = (int)length * sizeof(char); - // The string contains an ending null character; remove it before returning the value - string value = Encoding.Unicode.GetString(buffer, index, size).Substring(0, length - 1); - index += size; - return value; - } private UInt32 ExpectedSizeInBytes { get; set; } private UInt16 Future { get; set; } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs index 91bec0c7c..f77388240 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs @@ -40,26 +40,13 @@ namespace Microsoft.Diagnostics.NETCore.Client processInfo.RuntimeInstanceCookie = new Guid(cookieBuffer); index += GuidSizeInBytes; - processInfo.CommandLine = ReadString(payload, ref index); - processInfo.OperatingSystem = ReadString(payload, ref index); - processInfo.ProcessArchitecture = ReadString(payload, ref index); + processInfo.CommandLine = IpcHelpers.ReadString(payload, ref index); + processInfo.OperatingSystem = IpcHelpers.ReadString(payload, ref index); + processInfo.ProcessArchitecture = IpcHelpers.ReadString(payload, ref index); return processInfo; } - private static string ReadString(byte[] buffer, ref int index) - { - // Length of the string of UTF-16 characters - int length = (int)BitConverter.ToUInt32(buffer, index); - index += sizeof(UInt32); - - int size = (int)length * sizeof(char); - // The string contains an ending null character; remove it before returning the value - string value = Encoding.Unicode.GetString(buffer, index, size).Substring(0, length - 1); - index += size; - return value; - } - public UInt64 ProcessId { get; private set; } public Guid RuntimeInstanceCookie { get; private set; } public string CommandLine { get; private set; } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/IpcHelpers.cs b/src/Microsoft.Diagnostics.NETCore.Client/IpcHelpers.cs new file mode 100644 index 000000000..08ddc7eaf --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/IpcHelpers.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal static class IpcHelpers + { + public static string ReadString(byte[] buffer, ref int index) + { + // Length of the string of UTF-16 characters + int length = (int)BitConverter.ToUInt32(buffer, index); + index += sizeof(UInt32); + + int size = (int)length * sizeof(char); + // The string contains an ending null character; remove it before returning the value + string value = Encoding.Unicode.GetString(buffer, index, size).Substring(0, length - 1); + index += size; + return value; + } + } +} diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs index 4b37afe62..1f8029799 100644 --- a/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs @@ -4,6 +4,7 @@ using System; +using System.Linq; using System.ComponentModel; using System.Diagnostics; using System.IO; @@ -11,6 +12,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Xunit.Abstractions; +using System.Collections.Generic; namespace Microsoft.Diagnostics.NETCore.Client { @@ -22,13 +24,14 @@ namespace Microsoft.Diagnostics.NETCore.Client private CancellationTokenSource cts; public TestRunner(string testExePath, ITestOutputHelper _outputHelper = null, - bool redirectError = false, bool redirectInput = false) + bool redirectError = false, bool redirectInput = false, Dictionary envVars = null) { startInfo = new ProcessStartInfo(CommonHelper.HostExe, testExePath); startInfo.UseShellExecute = false; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = redirectError; startInfo.RedirectStandardInput = redirectInput; + envVars?.ToList().ForEach(item => startInfo.Environment.Add(item.Key, item.Value)); outputHelper = _outputHelper; } -- 2.34.1