diff --git a/documentation/diagnostics-client-library-instructions.md b/documentation/diagnostics-client-library-instructions.md index 09fcfdd711..1ebddb12b4 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 6b11c75071..0cdc5c14b5 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs @@ -61,7 +61,7 @@ internal Task WaitForConnectionAsync(CancellationToken token) /// /// 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 @@ public EventPipeSession StartEventPipeSession(IEnumerable pro /// /// 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 @@ public EventPipeSession StartEventPipeSession(EventPipeProvider provider, bool r /// 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 @@ public void WriteDump(DumpType dumpType, string dumpPath, bool logDumpGeneration /// 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 @@ public void AttachProfiler(TimeSpan attachTimeout, Guid profilerGuid, string pro 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 @@ public void AttachProfiler(TimeSpan attachTimeout, Guid profilerGuid, string pro 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 @@ public void AttachProfiler(TimeSpan attachTimeout, Guid profilerGuid, string pro // 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 @@ internal void ResumeRuntimeFallback() } } - 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 @@ public Dictionary GetProcessEnvironment() 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 @@ static IEnumerable GetAllPublishedProcesses() 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 ca59ba2558..68442de6cf 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs @@ -51,6 +51,7 @@ internal enum DumpCommandId : byte internal enum ProfilerCommandId : byte { AttachProfiler = 0x01, + StartupProfiler = 0x02, } internal enum ProcessCommandId : byte @@ -58,5 +59,6 @@ internal enum ProcessCommandId : byte 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 372d49ef3d..00b50980ac 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 318e1d96fc..b3985e49c4 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs @@ -42,7 +42,7 @@ public static ProcessEnvironmentHelper Parse(byte[] payload) 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 @@ public static ProcessEnvironmentHelper Parse(byte[] payload) 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 91bec0c7c9..f77388240d 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs @@ -40,26 +40,13 @@ public static ProcessInfo Parse(byte[] payload) 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 0000000000..08ddc7eafa --- /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 4b37afe627..1f8029799f 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.Threading; using System.Threading.Tasks; using Xunit.Abstractions; +using System.Collections.Generic; namespace Microsoft.Diagnostics.NETCore.Client { @@ -22,13 +24,14 @@ public class TestRunner : IDisposable 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; }