Add new DiagnosticClient commands for IPC features (#2268)
authorDavid Mason <davmason@microsoft.com>
Mon, 28 Jun 2021 18:17:35 +0000 (11:17 -0700)
committerGitHub <noreply@github.com>
Mon, 28 Jun 2021 18:17:35 +0000 (11:17 -0700)
Client commands for dotnet/runtime#52175 and dotnet/runtime#52567

documentation/diagnostics-client-library-instructions.md
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs
src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs
src/Microsoft.Diagnostics.NETCore.Client/IpcHelpers.cs [new file with mode: 0644]
src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs

index 09fcfdd7119d1888a151ac231409079f580706dd..1ebddb12b416945d7c55d3a7d77fbba1766b11af 100644 (file)
@@ -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
 
index 6b11c75071a6e88464bd374e95273f260ac849eb..0cdc5c14b544bb3fb32b9fcfdc34d0f1ca7cddc0 100644 (file)
@@ -61,7 +61,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
         /// <returns>
         /// An EventPipeSession object representing the EventPipe session that just started.
         /// </returns> 
-        public EventPipeSession StartEventPipeSession(IEnumerable<EventPipeProvider> providers, bool requestRundown=true, int circularBufferMB=256)
+        public EventPipeSession StartEventPipeSession(IEnumerable<EventPipeProvider> providers, bool requestRundown = true, int circularBufferMB = 256)
         {
             return new EventPipeSession(_endpoint, providers, requestRundown, circularBufferMB);
         }
@@ -75,7 +75,7 @@ namespace Microsoft.Diagnostics.NETCore.Client
         /// <returns>
         /// An EventPipeSession object representing the EventPipe session that just started.
         /// </returns> 
-        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
         /// <param name="dumpType">Type of the dump to be generated</param>
         /// <param name="dumpPath">Full path to the dump to be generated. By default it is /tmp/coredump.{pid}</param>
         /// <param name="logDumpGeneration">When set to true, display the dump generation debug log to the console.</param>
-        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
         /// <param name="profilerGuid">Guid for the profiler to be attached</param>
         /// <param name="profilerPath">Path to the profiler to be attached</param>
         /// <param name="additionalData">Additional data to be passed to the profiler</param>
-        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()
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <param name="profilerGuid">Guid for the profiler to be attached</param>
+        /// <param name="profilerPath">Path to the profiler to be attached</param>
+        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()
+        /// <summary>
+        /// Tell the runtime to resume execution after being paused for "reverse server" mode.
+        /// </summary>
+        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()
+        /// <summary>
+        /// Set an environment variable in the target process.
+        /// </summary>
+        /// <param name="name">The name of the environment variable to set.</param>
+        /// <param name="value">The value of the environment variable to set.</param>
+        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<string,string> GetProcessEnvironment()
+        /// <summary>
+        /// Gets all environement variables and their values from the target process.
+        /// </summary>
+        /// <returns>A dictionary containing all of the environment variables defined in the target process.</returns>
+        public Dictionary<string, string> 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<Dictionary<string,string>> envTask = helper.ReadEnvironmentAsync(continuation);
+                    Task<Dictionary<string, string>> 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>(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, T2>(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, T2, T3>(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, T2, T3, T4>(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>(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.");
+            }
+        }
     }
 }
index ca59ba2558875fe1745f08f73fef2ef1b068cf0b..68442de6cfe60265a9dfdda6f04a34982259d432 100644 (file)
@@ -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,
     }
 }
index 372d49ef3d856b8d8d9fb4773ef810ef3f46f144..00b50980ac061292197d0b47e93d7522ffc28a4d 100644 (file)
@@ -15,11 +15,12 @@ namespace Microsoft.Diagnostics.NETCore.Client
     /// </summary>
     internal enum DiagnosticsIpcError : uint
     {
+        InvalidArgument       = 0x80070057,
         ProfilerAlreadyActive = 0x8013136A,
         BadEncoding           = 0x80131384,
         UnknownCommand        = 0x80131385,
         UnknownMagic          = 0x80131386,
-        UnknownError          = 0x80131387
+        UnknownError          = 0x80131387,
     }
 
     /// <summary>
index 318e1d96fce3ddc27f821b13c7f6f7e42b526e75..b3985e49c4eccc58cefcde7e6cc12e6511ff2e28 100644 (file)
@@ -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; }
index 91bec0c7c962653270d85bfbae2c63c8f719c6cc..f77388240d8f9b8820d806f5d3470e918619ce81 100644 (file)
@@ -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 (file)
index 0000000..08ddc7e
--- /dev/null
@@ -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;
+        }
+    }
+}
index 4b37afe6277136ca2775af28176e87c887adc10a..1f8029799fb7e577ddff0cc65041fd3b9bee4a63 100644 (file)
@@ -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<string, string> 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;
         }