File: DiagnosticsClient\DiagnosticsClient.cs
Web Access
Project: 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.BadEncoding:
                            throw new BadEncodingException($"{operationName} failed - the target runtime rejected the request as invalid (bad encoding), likely because it does not support one or more of the configured options. Retry with options supported by the target runtime's version.");

                        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,
        }
    }
}