File: DiagnosticsClient\EventPipeSession.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.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Diagnostics.NETCore.Client
{
    public class EventPipeSession : IDisposable
    {
        //! This is CoreCLR specific keywords for native ETW events (ending up in event pipe).
        //! The keywords below seems to correspond to:
        //!  GCKeyword                          (0x00000001)
        //!  LoaderKeyword                      (0x00000008)
        //!  JitKeyword                         (0x00000010)
        //!  NgenKeyword                        (0x00000020)
        //!  unused_keyword                     (0x00000100)
        //!  JittedMethodILToNativeMapKeyword   (0x00020000)
        //!  ThreadTransferKeyword              (0x80000000)
        internal const long DefaultRundownKeyword = 0x80020139;

        private ulong _sessionId;
        private IpcEndpoint _endpoint;
        private bool _disposedValue; // To detect redundant calls
        private bool _stopped; // To detect redundant calls
        private readonly IpcResponse _response;

        private EventPipeSession(IpcEndpoint endpoint, IpcResponse response, ulong sessionId)
        {
            _endpoint = endpoint;
            _response = response;
            _sessionId = sessionId;
        }

        public Stream EventStream => _response.Continuation;

        internal static EventPipeSession Start(IpcEndpoint endpoint, EventPipeSessionConfiguration config)
        {
            IpcMessage requestMessage = CreateStartMessage(config);
            IpcResponse? response = IpcClient.SendMessageGetContinuation(endpoint, requestMessage);
            return CreateSessionFromResponse(endpoint, ref response, nameof(Start));
        }

        internal static async Task<EventPipeSession> StartAsync(IpcEndpoint endpoint, EventPipeSessionConfiguration config, CancellationToken cancellationToken)
        {
            IpcMessage requestMessage = CreateStartMessage(config);
            IpcResponse? response = await IpcClient.SendMessageGetContinuationAsync(endpoint, requestMessage, cancellationToken).ConfigureAwait(false);
            return CreateSessionFromResponse(endpoint, ref response, nameof(StartAsync));
        }

        ///<summary>
        /// Stops the given session
        ///</summary>
        public void Stop()
        {
            if (TryCreateStopMessage(out IpcMessage requestMessage))
            {
                try
                {
                    IpcMessage response = IpcClient.SendMessage(_endpoint, requestMessage);

                    DiagnosticsClient.ValidateResponseMessage(response, nameof(Stop));
                }
                // On non-abrupt exits (i.e. the target process has already exited and pipe is gone, sending Stop command will fail).
                catch (IOException)
                {
                    throw new ServerNotAvailableException("Could not send Stop command. The target process may have exited.");
                }
            }
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            if (TryCreateStopMessage(out IpcMessage requestMessage))
            {
                try
                {
                    IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, requestMessage, cancellationToken).ConfigureAwait(false);

                    DiagnosticsClient.ValidateResponseMessage(response, nameof(StopAsync));
                }
                // On non-abrupt exits (i.e. the target process has already exited and pipe is gone, sending Stop command will fail).
                catch (IOException)
                {
                    throw new ServerNotAvailableException("Could not send Stop command. The target process may have exited.");
                }
            }
        }

        private static IpcMessage CreateStartMessage(EventPipeSessionConfiguration config)
        {
            // To keep backward compatibility with older runtimes we only use newer serialization format when needed
            EventPipeCommandId command;
            byte[] payload;
            if (config.RundownKeyword != DefaultRundownKeyword && config.RundownKeyword != 0)
            {
                // V4 has added support to specify rundown keyword
                command = EventPipeCommandId.CollectTracing4;
                payload = config.SerializeV4();
            }
            else if (!config.RequestStackwalk)
            {
                // V3 has added support to disable the stacktraces
                command = EventPipeCommandId.CollectTracing3;
                payload = config.SerializeV3();
            }
            else
            {
                command = EventPipeCommandId.CollectTracing2;
                payload = config.SerializeV2();
            }

            return new IpcMessage(DiagnosticsServerCommandSet.EventPipe, (byte)command, payload);
        }

        private static EventPipeSession CreateSessionFromResponse(IpcEndpoint endpoint, ref IpcResponse? response, string operationName)
        {
            try
            {
                DiagnosticsClient.ValidateResponseMessage(response.Value.Message, operationName);

                ulong sessionId = BinaryPrimitives.ReadUInt64LittleEndian(new ReadOnlySpan<byte>(response.Value.Message.Payload, 0, 8));

                EventPipeSession session = new(endpoint, response.Value, sessionId);
                response = null;
                return session;
            }
            finally
            {
                response?.Dispose();
            }
        }

        private bool TryCreateStopMessage(out IpcMessage stopMessage)
        {
            Debug.Assert(_sessionId > 0);

            // Do not issue another Stop command if it has already been issued for this session instance.
            if (_stopped)
            {
                stopMessage = null;
                return false;
            }
            else
            {
                _stopped = true;
            }

            byte[] payload = BitConverter.GetBytes(_sessionId);
            if (!BitConverter.IsLittleEndian)
            {
                Array.Reverse(payload);
            }

            stopMessage = new IpcMessage(DiagnosticsServerCommandSet.EventPipe, (byte)EventPipeCommandId.StopTracing, payload);

            return true;
        }

        protected virtual void Dispose(bool disposing)
        {
            // Do not call Stop() here. Trying to do so now might block indefinitely if the runtime is unresponsive and we don't want blocking behavior in Dispose().
            // If the caller wants to ensure that all rundown events are captured they should call Stop() first, then process the EventStream until it is complete, then call Dispose().

            if (!_disposedValue)
            {
                if (disposing)
                {
                    _response.Dispose();
                }
                _disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
        }
    }
}