File: DiagnosticsClient\DiagnosticsClient.cs
Web Access
Project: src\src\diagnostics\src\Microsoft.Diagnostics.NETCore.Client\Microsoft.Diagnostics.NETCore.Client.csproj (Microsoft.Diagnostics.NETCore.Client)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Diagnostics.NETCore.Client
{
    /// <summary>
    /// This is a top-level class that contains methods to send various diagnostics command to the runtime.
    /// </summary>
    public sealed class DiagnosticsClient
    {
        private const int DefaultCircularBufferMB = 256;

        private readonly IpcEndpoint _endpoint;

        public DiagnosticsClient(int processId) :
            this(new PidIpcEndpoint(processId))
        {
        }

        internal DiagnosticsClient(IpcEndpointConfig config) :
            this(new DiagnosticPortIpcEndpoint(config))
        {
        }

        internal DiagnosticsClient(IpcEndpoint endpoint)
        {
            _endpoint = endpoint;
        }

        /// <summary>
        /// Wait for an available diagnostic endpoint to the runtime instance.
        /// </summary>
        /// <param name="timeout">The amount of time to wait before cancelling the wait for the connection.</param>
        internal void WaitForConnection(TimeSpan timeout)
        {
            _endpoint.WaitForConnection(timeout);
        }

        /// <summary>
        /// Wait for an available diagnostic endpoint to the runtime instance.
        /// </summary>
        /// <param name="token">The token to monitor for cancellation requests.</param>
        /// <returns>
        /// A task the completes when a diagnostic endpoint to the runtime instance becomes available.
        /// </returns>
        internal Task WaitForConnectionAsync(CancellationToken token)
        {
            return _endpoint.WaitForConnectionAsync(token);
        }

        /// <summary>
        /// Start tracing the application and return an EventPipeSession object
        /// </summary>
        /// <param name="providers">An IEnumerable containing the list of Providers to turn on.</param>
        /// <param name="requestRundown">If true, request rundown events from the runtime</param>
        /// <param name="circularBufferMB">The size of the runtime's buffer for collecting events in MB</param>
        /// <returns>
        /// An EventPipeSession object representing the EventPipe session that just started.
        /// </returns>
        public EventPipeSession StartEventPipeSession(IEnumerable<EventPipeProvider> providers, bool requestRundown = true, int circularBufferMB = DefaultCircularBufferMB)
        {
            EventPipeSessionConfiguration config = new(providers, circularBufferMB, requestRundown: requestRundown, requestStackwalk: true);
            return EventPipeSession.Start(_endpoint, config);
        }

        /// <summary>
        /// Start tracing the application and return an EventPipeSession object
        /// </summary>
        /// <param name="provider">An EventPipeProvider to turn on.</param>
        /// <param name="requestRundown">If true, request rundown events from the runtime</param>
        /// <param name="circularBufferMB">The size of the runtime's buffer for collecting events in MB</param>
        /// <returns>
        /// An EventPipeSession object representing the EventPipe session that just started.
        /// </returns>
        public EventPipeSession StartEventPipeSession(EventPipeProvider provider, bool requestRundown = true, int circularBufferMB = DefaultCircularBufferMB)
        {
            EventPipeSessionConfiguration config = new(new[] {provider}, circularBufferMB, requestRundown: requestRundown, requestStackwalk: true);
            return EventPipeSession.Start(_endpoint, config);
        }

        /// <summary>
        /// Start tracing the application and return an EventPipeSession object
        /// </summary>
        /// <param name="config">The configuration for start tracing.</param>
        /// <returns>
        /// An EventPipeSession object representing the EventPipe session that just started.
        /// </returns>
        public EventPipeSession StartEventPipeSession(EventPipeSessionConfiguration config)
        {
            return EventPipeSession.Start(_endpoint, config);
        }

        /// <summary>
        /// Start tracing the application and return an EventPipeSession object
        /// </summary>
        /// <param name="providers">An IEnumerable containing the list of Providers to turn on.</param>
        /// <param name="requestRundown">If true, request rundown events from the runtime</param>
        /// <param name="circularBufferMB">The size of the runtime's buffer for collecting events in MB</param>
        /// <param name="token">The token to monitor for cancellation requests.</param>
        /// <returns>
        /// An EventPipeSession object representing the EventPipe session that just started.
        /// </returns>
        public Task<EventPipeSession> StartEventPipeSessionAsync(IEnumerable<EventPipeProvider> providers, bool requestRundown,
            int circularBufferMB = DefaultCircularBufferMB, CancellationToken token = default)
        {
            EventPipeSessionConfiguration config = new(providers, circularBufferMB, requestRundown: requestRundown, requestStackwalk: true);
            return EventPipeSession.StartAsync(_endpoint, config, token);
        }

        /// <summary>
        /// Start tracing the application and return an EventPipeSession object
        /// </summary>
        /// <param name="provider">An EventPipeProvider to turn on.</param>
        /// <param name="requestRundown">If true, request rundown events from the runtime</param>
        /// <param name="circularBufferMB">The size of the runtime's buffer for collecting events in MB</param>
        /// <param name="token">The token to monitor for cancellation requests.</param>
        /// <returns>
        /// An EventPipeSession object representing the EventPipe session that just started.
        /// </returns>
        public Task<EventPipeSession> StartEventPipeSessionAsync(EventPipeProvider provider, bool requestRundown,
            int circularBufferMB = DefaultCircularBufferMB, CancellationToken token = default)
        {
            EventPipeSessionConfiguration config = new(new[] {provider}, circularBufferMB, requestRundown: requestRundown, requestStackwalk: true);
            return EventPipeSession.StartAsync(_endpoint, config, token);
        }

        /// <summary>
        /// Start tracing the application and return an EventPipeSession object
        /// </summary>
        /// <param name="configuration">Configuration of this EventPipeSession</param>
        /// <param name="token">The token to monitor for cancellation requests.</param>
        /// <returns>
        /// An EventPipeSession object representing the EventPipe session that just started.
        /// </returns>
        public Task<EventPipeSession> StartEventPipeSessionAsync(EventPipeSessionConfiguration configuration, CancellationToken token)
        {
            return EventPipeSession.StartAsync(_endpoint, configuration, token);
        }

        /// <summary>
        /// Trigger a core dump generation.
        /// </summary>
        /// <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)
        {
            WriteDump(dumpType, dumpPath, logDumpGeneration ? WriteDumpFlags.LoggingEnabled : WriteDumpFlags.None);
        }

        /// <summary>
        /// Trigger a core dump generation.
        /// </summary>
        /// <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="flags">logging and crash report flags. On runtimes less than 6.0, only LoggingEnabled is supported.</param>
        public void WriteDump(DumpType dumpType, string dumpPath, WriteDumpFlags flags)
        {
            IpcMessage request = CreateWriteDumpMessage(DumpCommandId.GenerateCoreDump3, dumpType, dumpPath, flags);
            IpcMessage response = IpcClient.SendMessage(_endpoint, request);
            if (!ValidateResponseMessage(response, "Write dump", ValidateResponseOptions.UnknownCommandReturnsFalse | ValidateResponseOptions.ErrorMessageReturned))
            {
                request = CreateWriteDumpMessage(DumpCommandId.GenerateCoreDump2, dumpType, dumpPath, flags);
                response = IpcClient.SendMessage(_endpoint, request);
                if (!ValidateResponseMessage(response, "Write dump", ValidateResponseOptions.UnknownCommandReturnsFalse))
                {
                    if ((flags & ~WriteDumpFlags.LoggingEnabled) != 0)
                    {
                        throw new ArgumentException($"Only {nameof(WriteDumpFlags.LoggingEnabled)} flag is supported by this runtime version", nameof(flags));
                    }
                    request = CreateWriteDumpMessage(dumpType, dumpPath, logDumpGeneration: (flags & WriteDumpFlags.LoggingEnabled) != 0);
                    response = IpcClient.SendMessage(_endpoint, request);
                    ValidateResponseMessage(response, "Write dump");
                }
            }
        }

        /// <summary>
        /// Trigger a core dump generation.
        /// </summary>
        /// <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>
        /// <param name="token">The token to monitor for cancellation requests.</param>
        public Task WriteDumpAsync(DumpType dumpType, string dumpPath, bool logDumpGeneration, CancellationToken token)
        {
            return WriteDumpAsync(dumpType, dumpPath, logDumpGeneration ? WriteDumpFlags.LoggingEnabled : WriteDumpFlags.None, token);
        }

        /// <summary>
        /// Trigger a core dump generation.
        /// </summary>
        /// <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="flags">logging and crash report flags. On runtimes less than 6.0, only LoggingEnabled is supported.</param>
        /// <param name="token">The token to monitor for cancellation requests.</param>
        public async Task WriteDumpAsync(DumpType dumpType, string dumpPath, WriteDumpFlags flags, CancellationToken token)
        {
            IpcMessage request = CreateWriteDumpMessage(DumpCommandId.GenerateCoreDump3, dumpType, dumpPath, flags);
            IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
            if (!ValidateResponseMessage(response, "Write dump", ValidateResponseOptions.UnknownCommandReturnsFalse | ValidateResponseOptions.ErrorMessageReturned))
            {
                request = CreateWriteDumpMessage(DumpCommandId.GenerateCoreDump2, dumpType, dumpPath, flags);
                response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
                if (!ValidateResponseMessage(response, "Write dump", ValidateResponseOptions.UnknownCommandReturnsFalse))
                {
                    if ((flags & ~WriteDumpFlags.LoggingEnabled) != 0)
                    {
                        throw new ArgumentException($"Only {nameof(WriteDumpFlags.LoggingEnabled)} flag is supported by this runtime version", nameof(flags));
                    }
                    request = CreateWriteDumpMessage(dumpType, dumpPath, logDumpGeneration: (flags & WriteDumpFlags.LoggingEnabled) != 0);
                    response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
                    ValidateResponseMessage(response, "Write dump");
                }
            }
        }

        /// <summary>
        /// Attach a profiler.
        /// </summary>
        /// <param name="attachTimeout">Timeout for attaching the profiler</param>
        /// <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)
        {
            IpcMessage request = CreateAttachProfilerMessage(attachTimeout, profilerGuid, profilerPath, additionalData);
            IpcMessage response = IpcClient.SendMessage(_endpoint, request);
            ValidateResponseMessage(response, nameof(AttachProfiler));

            // The call to set up the pipe and send the message operates on a different timeout than attachTimeout, which is for the runtime.
            // We should eventually have a configurable timeout for the message passing, potentially either separately from the
            // runtime timeout or respect attachTimeout as one total duration.
        }

        internal async Task AttachProfilerAsync(TimeSpan attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData, CancellationToken token)
        {
            IpcMessage request = CreateAttachProfilerMessage(attachTimeout, profilerGuid, profilerPath, additionalData);
            IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
            ValidateResponseMessage(response, nameof(AttachProfilerAsync));
        }

        /// <summary>
        /// Set a profiler as the startup profiler. It is only valid to issue this command
        /// while the runtime is paused at startup.
        /// </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 request = CreateSetStartupProfilerMessage(profilerGuid, profilerPath);
            IpcMessage response = IpcClient.SendMessage(_endpoint, request);
            ValidateResponseMessage(response, nameof(SetStartupProfiler), ValidateResponseOptions.InvalidArgumentIsRequiresSuspension);
        }

        internal async Task SetStartupProfilerAsync(Guid profilerGuid, string profilerPath, CancellationToken token)
        {
            IpcMessage request = CreateSetStartupProfilerMessage(profilerGuid, profilerPath);
            IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
            ValidateResponseMessage(response, nameof(SetStartupProfilerAsync), ValidateResponseOptions.InvalidArgumentIsRequiresSuspension);
        }

        /// <summary>
        /// Tell the runtime to resume execution after being paused at startup.
        /// </summary>
        public void ResumeRuntime()
        {
            IpcMessage request = CreateResumeRuntimeMessage();
            IpcMessage response = IpcClient.SendMessage(_endpoint, request);
            ValidateResponseMessage(response, nameof(ResumeRuntime));
        }

        internal async Task ResumeRuntimeAsync(CancellationToken token)
        {
            IpcMessage request = CreateResumeRuntimeMessage();
            IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
            ValidateResponseMessage(response, nameof(ResumeRuntimeAsync));
        }

        /// <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 request = CreateSetEnvironmentVariableMessage(name, value);
            IpcMessage response = IpcClient.SendMessage(_endpoint, request);
            ValidateResponseMessage(response, nameof(SetEnvironmentVariable));
        }

        internal async Task SetEnvironmentVariableAsync(string name, string value, CancellationToken token)
        {
            IpcMessage request = CreateSetEnvironmentVariableMessage(name, value);
            IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
            ValidateResponseMessage(response, nameof(SetEnvironmentVariableAsync));
        }

        /// <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()
        {
            IpcMessage message = CreateProcessEnvironmentMessage();
            using IpcResponse response = IpcClient.SendMessageGetContinuation(_endpoint, message);
            ValidateResponseMessage(response.Message, nameof(GetProcessEnvironmentAsync));

            ProcessEnvironmentHelper helper = ProcessEnvironmentHelper.Parse(response.Message.Payload);
            return helper.ReadEnvironment(response.Continuation);
        }

        internal async Task<Dictionary<string, string>> GetProcessEnvironmentAsync(CancellationToken token)
        {
            IpcMessage message = CreateProcessEnvironmentMessage();
            using IpcResponse response = await IpcClient.SendMessageGetContinuationAsync(_endpoint, message, token).ConfigureAwait(false);
            ValidateResponseMessage(response.Message, nameof(GetProcessEnvironmentAsync));

            ProcessEnvironmentHelper helper = ProcessEnvironmentHelper.Parse(response.Message.Payload);
            return await helper.ReadEnvironmentAsync(response.Continuation, token).ConfigureAwait(false);
        }

        /// <summary>
        /// Loads the specified assembly with a StartupHook in the target process.
        /// </summary>
        /// <param name="startupHookPath">The path to the assembly containing the StartupHook.</param>
        /// <exception cref="ArgumentNullException">Thrown when <paramref name="startupHookPath"/> is null or empty.</exception>
        public void ApplyStartupHook(string startupHookPath)
        {
            if (string.IsNullOrEmpty(startupHookPath))
            {
                throw new ArgumentNullException(nameof(startupHookPath));
            }

            IpcMessage message = CreateApplyStartupHookMessage(startupHookPath);
            IpcMessage response = IpcClient.SendMessage(_endpoint, message);
            ValidateResponseMessage(response, nameof(ApplyStartupHook));
        }

        /// <summary>
        /// Loads the specified assembly with a StartupHook in the target process.
        /// </summary>
        /// <param name="startupHookPath">The path to the assembly containing the StartupHook.</param>
        /// <param name="token">The token to monitor for cancellation requests.</param>
        /// <exception cref="ArgumentNullException">Thrown when <paramref name="startupHookPath"/> is null or empty.</exception>
        public async Task ApplyStartupHookAsync(string startupHookPath, CancellationToken token)
        {
            if (string.IsNullOrEmpty(startupHookPath))
            {
                throw new ArgumentNullException(nameof(startupHookPath));
            }

            IpcMessage message = CreateApplyStartupHookMessage(startupHookPath);
            IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, message, token).ConfigureAwait(false);
            ValidateResponseMessage(response, nameof(ApplyStartupHookAsync));
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="type"></param>
        public void EnablePerfMap(PerfMapType type)
        {
            IpcMessage request = CreateEnablePerfMapMessage(type);
            IpcMessage response = IpcClient.SendMessage(_endpoint, request);
            ValidateResponseMessage(response, nameof(EnablePerfMap));
        }

        internal async Task EnablePerfMapAsync(PerfMapType type, CancellationToken token)
        {
            IpcMessage request = CreateEnablePerfMapMessage(type);
            IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
            ValidateResponseMessage(response, nameof(EnablePerfMapAsync));
        }

        /// <summary>
        ///
        /// </summary>
        public void DisablePerfMap()
        {
            IpcMessage request = CreateDisablePerfMapMessage();
            IpcMessage response = IpcClient.SendMessage(_endpoint, request);
            ValidateResponseMessage(response, nameof(DisablePerfMap));
        }

        internal async Task DisablePerfMapAsync(CancellationToken token)
        {
            IpcMessage request = CreateDisablePerfMapMessage();
            IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, request, token).ConfigureAwait(false);
            ValidateResponseMessage(response, nameof(DisablePerfMapAsync));
        }

        /// <summary>
        /// Get all the active processes that can be attached to.
        /// </summary>
        /// <returns>
        /// IEnumerable of all the active process IDs.
        /// </returns>
        public static IEnumerable<int> GetPublishedProcesses()
        {
            HashSet<int> discoveredPids = new();

            foreach (int pid in GetLocalPublishedProcesses())
            {
                discoveredPids.Add(pid);
            }

            foreach (int pid in GetProcPublishedProcesses())
            {
                discoveredPids.Add(pid);
            }

            return discoveredPids;
        }

        /// <summary>
        /// Discovers .NET processes with diagnostic sockets in the local IPC root path.
        /// </summary>
        private static IEnumerable<int> GetLocalPublishedProcesses()
        {
            List<int> pids = new();

            string[] files;
            try
            {
                files = Directory.GetFiles(PidIpcEndpoint.IpcRootPath);
            }
            catch (UnauthorizedAccessException ex)
            {
                if (PidIpcEndpoint.IpcRootPath.StartsWith(@"\\.\pipe", StringComparison.Ordinal))
                {
                    throw new DiagnosticsClientException($"Enumerating {PidIpcEndpoint.IpcRootPath} is not authorized", ex);
                }

                throw;
            }

            foreach (string port in files)
            {
                string fileName = new FileInfo(port).Name;
                Match match = Regex.Match(fileName, PidIpcEndpoint.DiagnosticsPortPattern);
                if (!match.Success)
                {
                    continue;
                }

                string group = match.Groups[1].Value;
                if (!int.TryParse(group, NumberStyles.Integer, CultureInfo.InvariantCulture, out int processId))
                {
                    continue;
                }

                if (!PidIpcEndpoint.CheckProcessExists(processId))
                {
                    continue;
                }

                pids.Add(processId);
            }

            return pids;
        }

        /// <summary>
        /// Discovers .NET processes via /proc that aren't found by the local IPC root scan.
        /// Finds cross-namespace processes and same-namespace processes with different TMPDIR.
        /// Linux-only; returns empty on other platforms.
        /// </summary>
        private static IEnumerable<int> GetProcPublishedProcesses()
        {
            List<int> discoveredPids = new();

            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                return discoveredPids;
            }

            IEnumerable<string> procEntries;
            try
            {
                procEntries = Directory.EnumerateDirectories(PidIpcEndpoint.ProcPath);
            }
            catch (UnauthorizedAccessException)
            {
                return discoveredPids;
            }

            foreach (string procEntry in procEntries)
            {
                if (!int.TryParse(Path.GetFileName(procEntry), NumberStyles.Integer, CultureInfo.InvariantCulture, out int hostPid))
                {
                    continue;
                }

                if (!PidIpcEndpoint.CheckProcessExists(hostPid))
                {
                    continue;
                }

                string targetTmpDir = PidIpcEndpoint.GetProcessTmpDir(hostPid, out _);

                if (PidIpcEndpoint.TryGetNamespacePid(hostPid, out int nsPid))
                {
                    // Cross-namespace: search via /proc/{pid}/root/
                    string crossNsDir = Path.Combine(PidIpcEndpoint.GetProcessRootPath(hostPid), targetTmpDir.TrimStart(Path.DirectorySeparatorChar));
                    if (PidIpcEndpoint.TryResolveAddress(crossNsDir, nsPid, out _))
                    {
                        discoveredPids.Add(hostPid);
                    }
                }
                else if (!string.Equals(targetTmpDir, PidIpcEndpoint.IpcRootPath, StringComparison.Ordinal))
                {
                    // Same namespace but different TMPDIR
                    if (PidIpcEndpoint.TryResolveAddress(targetTmpDir, hostPid, out _))
                    {
                        discoveredPids.Add(hostPid);
                    }
                }
            }

            return discoveredPids;
        }

        internal ProcessInfo GetProcessInfo()
        {
            // Attempt to get ProcessInfo v3
            ProcessInfo processInfo = TryGetProcessInfo3();
            if (null != processInfo)
            {
                return processInfo;
            }

            // Attempt to get ProcessInfo v2
            processInfo = TryGetProcessInfo2();
            if (null != processInfo)
            {
                return processInfo;
            }

            IpcMessage request = CreateProcessInfoMessage();
            using IpcResponse response = IpcClient.SendMessageGetContinuation(_endpoint, request);
            return GetProcessInfoFromResponse(response, nameof(GetProcessInfo));
        }

        internal async Task<ProcessInfo> GetProcessInfoAsync(CancellationToken token)
        {
            // Attempt to get ProcessInfo v3
            ProcessInfo processInfo = await TryGetProcessInfo3Async(token).ConfigureAwait(false);
            if (null != processInfo)
            {
                return processInfo;
            }

            // Attempt to get ProcessInfo v2
            processInfo = await TryGetProcessInfo2Async(token).ConfigureAwait(false);
            if (null != processInfo)
            {
                return processInfo;
            }

            IpcMessage request = CreateProcessInfoMessage();
            using IpcResponse response = await IpcClient.SendMessageGetContinuationAsync(_endpoint, request, token).ConfigureAwait(false);
            return GetProcessInfoFromResponse(response, nameof(GetProcessInfoAsync));
        }

        private ProcessInfo TryGetProcessInfo2()
        {
            IpcMessage request = CreateProcessInfo2Message();
            using IpcResponse response2 = IpcClient.SendMessageGetContinuation(_endpoint, request);
            return TryGetProcessInfo2FromResponse(response2, nameof(GetProcessInfo));
        }

        private async Task<ProcessInfo> TryGetProcessInfo2Async(CancellationToken token)
        {
            IpcMessage request = CreateProcessInfo2Message();
            using IpcResponse response2 = await IpcClient.SendMessageGetContinuationAsync(_endpoint, request, token).ConfigureAwait(false);
            return TryGetProcessInfo2FromResponse(response2, nameof(GetProcessInfoAsync));
        }

        private ProcessInfo TryGetProcessInfo3()
        {
            IpcMessage request = CreateProcessInfo3Message();
            using IpcResponse response2 = IpcClient.SendMessageGetContinuation(_endpoint, request);
            return TryGetProcessInfo3FromResponse(response2, nameof(GetProcessInfo));
        }

        private async Task<ProcessInfo> TryGetProcessInfo3Async(CancellationToken token)
        {
            IpcMessage request = CreateProcessInfo3Message();
            using IpcResponse response2 = await IpcClient.SendMessageGetContinuationAsync(_endpoint, request, token).ConfigureAwait(false);
            return TryGetProcessInfo3FromResponse(response2, nameof(GetProcessInfoAsync));
        }

        private static byte[] SerializePayload<T>(T arg)
        {
            using (MemoryStream stream = new())
            using (BinaryWriter writer = new(stream))
            {
                SerializePayloadArgument(arg, writer);

                writer.Flush();
                return stream.ToArray();
            }
        }

        private static byte[] SerializePayload<T1, T2>(T1 arg1, T2 arg2)
        {
            using (MemoryStream stream = new())
            using (BinaryWriter writer = new(stream))
            {
                SerializePayloadArgument(arg1, writer);
                SerializePayloadArgument(arg2, writer);

                writer.Flush();
                return stream.ToArray();
            }
        }

        private static byte[] SerializePayload<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
        {
            using (MemoryStream stream = new())
            using (BinaryWriter writer = new(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 (MemoryStream stream = new())
            using (BinaryWriter writer = new(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 ? (uint)1 : 0;
                writer.Write(uiValue);
            }
            else if (typeof(T) == typeof(Guid))
            {
                Guid guidVal = (Guid)((object)obj);
                writer.Write(guidVal.ToByteArray());
            }
            else if (typeof(T) == typeof(byte[]))
            {
                byte[] byteArray = (byte[])((object)obj);
                uint length = byteArray == null ? 0U : (uint)byteArray.Length;
                writer.Write(length);

                if (length > 0)
                {
                    writer.Write(byteArray);
                }
            }
            else
            {
                throw new ArgumentException($"Type {obj.GetType()} is not supported in SerializePayloadArgument, please add it.");
            }
        }

        private static IpcMessage CreateAttachProfilerMessage(TimeSpan attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData)
        {
            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((uint)attachTimeout.TotalSeconds, profilerGuid, profilerPath, additionalData);
            return new IpcMessage(DiagnosticsServerCommandSet.Profiler, (byte)ProfilerCommandId.AttachProfiler, serializedConfiguration);
        }

        private static IpcMessage CreateProcessEnvironmentMessage()
        {
            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessEnvironment);
        }

        private static IpcMessage CreateProcessInfoMessage()
        {
            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessInfo);
        }

        private static IpcMessage CreateProcessInfo2Message()
        {
            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessInfo2);
        }

        private static IpcMessage CreateProcessInfo3Message()
        {
            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessInfo3);
        }

        private static IpcMessage CreateResumeRuntimeMessage()
        {
            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.ResumeRuntime);
        }

        private static IpcMessage CreateSetEnvironmentVariableMessage(string name, string value)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentException($"{nameof(name)} must be non-null.");
            }

            byte[] serializedConfiguration = SerializePayload(name, value);
            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.SetEnvironmentVariable, serializedConfiguration);
        }

        private static IpcMessage CreateSetStartupProfilerMessage(Guid profilerGuid, string profilerPath)
        {
            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);
            return new IpcMessage(DiagnosticsServerCommandSet.Profiler, (byte)ProfilerCommandId.StartupProfiler, serializedConfiguration);
        }

        private static IpcMessage CreateWriteDumpMessage(DumpType dumpType, string dumpPath, bool logDumpGeneration)
        {
            if (string.IsNullOrEmpty(dumpPath))
            {
                throw new ArgumentNullException($"{nameof(dumpPath)} required");
            }

            // Quote the path to handle spaces correctly in createdump on Windows only
            // Only add quotes if the path is not already quoted
            // This is only needed on Windows where the runtime builds the command line for createdump
            string pathToUse = dumpPath;
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                pathToUse = dumpPath.StartsWith("\"") && dumpPath.EndsWith("\"") ? dumpPath : $"\"{dumpPath}\"";
            }
            byte[] payload = SerializePayload(pathToUse, (uint)dumpType, logDumpGeneration);
            return new IpcMessage(DiagnosticsServerCommandSet.Dump, (byte)DumpCommandId.GenerateCoreDump, payload);
        }

        private static IpcMessage CreateWriteDumpMessage(DumpCommandId command, DumpType dumpType, string dumpPath, WriteDumpFlags flags)
        {
            if (string.IsNullOrEmpty(dumpPath))
            {
                throw new ArgumentNullException($"{nameof(dumpPath)} required");
            }

            // Quote the path to handle spaces correctly in createdump on Windows only
            // Only add quotes if the path is not already quoted
            // This is only needed on Windows where the runtime builds the command line for createdump
            string pathToUse = dumpPath;
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                pathToUse = dumpPath.StartsWith("\"") && dumpPath.EndsWith("\"") ? dumpPath : $"\"{dumpPath}\"";
            }
            byte[] payload = SerializePayload(pathToUse, (uint)dumpType, (uint)flags);
            return new IpcMessage(DiagnosticsServerCommandSet.Dump, (byte)command, payload);
        }

        private static IpcMessage CreateApplyStartupHookMessage(string startupHookPath)
        {
            if (string.IsNullOrEmpty(startupHookPath))
            {
                throw new ArgumentException($"{nameof(startupHookPath)} required");
            }

            byte[] serializedConfiguration = SerializePayload(startupHookPath);

            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.ApplyStartupHook, serializedConfiguration);
        }

        private static IpcMessage CreateEnablePerfMapMessage(PerfMapType type)
        {
            byte[] payload = SerializePayload((uint)type);
            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.EnablePerfMap, payload);
        }

        private static IpcMessage CreateDisablePerfMapMessage()
        {
            return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.DisablePerfMap);
        }

        private static ProcessInfo GetProcessInfoFromResponse(IpcResponse response, string operationName)
        {
            ValidateResponseMessage(response.Message, operationName);

            return ProcessInfo.ParseV1(response.Message.Payload);
        }

        private static ProcessInfo TryGetProcessInfo2FromResponse(IpcResponse response, string operationName)
        {
            if (!ValidateResponseMessage(response.Message, operationName, ValidateResponseOptions.UnknownCommandReturnsFalse))
            {
                return null;
            }

            return ProcessInfo.ParseV2(response.Message.Payload);
        }

        private static ProcessInfo TryGetProcessInfo3FromResponse(IpcResponse response, string operationName)
        {
            if (!ValidateResponseMessage(response.Message, operationName, ValidateResponseOptions.UnknownCommandReturnsFalse))
            {
                return null;
            }

            return ProcessInfo.ParseV3(response.Message.Payload);
        }

        internal static bool ValidateResponseMessage(IpcMessage responseMessage, string operationName, ValidateResponseOptions options = ValidateResponseOptions.None)
        {
            switch ((DiagnosticsServerResponseId)responseMessage.Header.CommandId)
            {
                case DiagnosticsServerResponseId.OK:
                    return true;

                case DiagnosticsServerResponseId.Error:
                    uint hr = BinaryPrimitives.ReadUInt32LittleEndian(new ReadOnlySpan<byte>(responseMessage.Payload, 0, 4));
                    int index = sizeof(uint);
                    string message = null;
                    switch (hr)
                    {
                        case (uint)DiagnosticsIpcError.UnknownCommand:
                            if (options.HasFlag(ValidateResponseOptions.UnknownCommandReturnsFalse))
                            {
                                return false;
                            }
                            throw new UnsupportedCommandException($"{operationName} failed - Command is not supported.");

                        case (uint)DiagnosticsIpcError.ProfilerAlreadyActive:
                            throw new ProfilerAlreadyActiveException($"{operationName} failed - A profiler is already loaded.");

                        case (uint)DiagnosticsIpcError.InvalidArgument:
                            if (options.HasFlag(ValidateResponseOptions.InvalidArgumentIsRequiresSuspension))
                            {
                                throw new ServerErrorException($"{operationName} failed - The runtime must be suspended for this command.");
                            }
                            throw new UnsupportedCommandException($"{operationName} failed - Invalid command argument.");

                        case (uint)DiagnosticsIpcError.NotSupported:
                            message = $"{operationName} - Not supported by this runtime.";
                            break;

                        default:
                            break;
                    }
                    // Check if the command can return an error message and if the payload is big enough to contain the
                    // error code (uint) and the string length (uint).
                    if (options.HasFlag(ValidateResponseOptions.ErrorMessageReturned) && responseMessage.Payload.Length >= (sizeof(uint) * 2))
                    {
                        message = IpcHelpers.ReadString(responseMessage.Payload, ref index);
                    }
                    if (string.IsNullOrWhiteSpace(message))
                    {
                        message = $"{operationName} failed - HRESULT: 0x{hr:X8}.";
                    }
                    throw new ServerErrorException(message);

                default:
                    throw new ServerErrorException($"{operationName} failed - Server responded with unknown response.");
            }
        }

        [Flags]
        internal enum ValidateResponseOptions
        {
            None = 0x0,
            UnknownCommandReturnsFalse = 0x1,
            InvalidArgumentIsRequiresSuspension = 0x2,
            ErrorMessageReturned = 0x4,
        }
    }
}