// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using Microsoft.Diagnostics.Tools.RuntimeClient.DiagnosticsIpc;
using System;
using System.Diagnostics;
using System.IO;
/// <summary>
/// Controls the contents of the dump
/// </summary>
- public enum DumpType : int
+ public enum DumpType : uint
{
Normal = 1,
WithHeap = 2,
/// <param name="dumpName">Path and file name of core dump</param>
/// <param name="dumpType">Type of dump</param>
/// <param name="diagnostics">If true, log to console the dump generation diagnostics</param>
- /// <returns>HRESULT</returns>
+ /// <returns>DiagnosticServerErrorCode</returns>
public static int GenerateCoreDump(int processId, string dumpName, DumpType dumpType, bool diagnostics)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
if (string.IsNullOrEmpty(dumpName))
throw new ArgumentNullException($"{nameof(dumpName)} required");
- var header = new MessageHeader {
- RequestType = DiagnosticMessageType.GenerateCoreDump,
- Pid = (uint)Process.GetCurrentProcess().Id,
- };
- byte[] serializedConfiguration;
- using (var stream = new MemoryStream())
- serializedConfiguration = SerializeCoreDump(header, stream, dumpName, dumpType, diagnostics);
+ var payload = SerializeCoreDump(dumpName, dumpType, diagnostics);
+ var message = new IpcMessage(DiagnosticServerCommandSet.Dump, (byte)DumpCommandId.GenerateCoreDump, payload);
- return (int)EventPipeClient.SendCommand(processId, serializedConfiguration);
+ var response = IpcClient.SendMessage(processId, message);
+
+ var hr = 0;
+ switch ((DiagnosticServerCommandId)response.Header.CommandId)
+ {
+ case DiagnosticServerCommandId.Error:
+ case DiagnosticServerCommandId.OK:
+ hr = BitConverter.ToInt32(response.Payload);
+ break;
+ default:
+ return -1;
+ }
+
+ return hr;
}
/// <summary>
Pid = (uint)Process.GetCurrentProcess().Id,
};
- byte[] serializedConfiguration;
- using (var stream = new MemoryStream())
+ byte[] serializedConfiguration = SerializeProfilerAttach(attachTimeout, profilerGuid, profilerPath, additionalData);
+ var message = new IpcMessage(DiagnosticServerCommandSet.Profiler, (byte)ProfilerCommandId.AttachProfiler, serializedConfiguration);
+
+ var response = IpcClient.SendMessage(processId, message);
+
+ var hr = 0;
+ switch ((DiagnosticServerCommandId)response.Header.CommandId)
{
- serializedConfiguration = SerializeProfilerAttach(header, stream, attachTimeout, profilerGuid, profilerPath, additionalData);
+ case DiagnosticServerCommandId.Error:
+ case DiagnosticServerCommandId.OK:
+ hr = BitConverter.ToInt32(response.Payload);
+ break;
+ default:
+ hr = -1;
+ break;
}
// TODO: 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.
- return (int)EventPipeClient.SendCommand(processId, serializedConfiguration);
-
+ return hr;
}
- private static byte[] SerializeProfilerAttach(MessageHeader header, MemoryStream stream, uint attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData)
+ private static byte[] SerializeProfilerAttach(uint attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData)
{
- using (var bw = new BinaryWriter(stream))
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
{
- bw.Write((uint)header.RequestType);
- bw.Write(header.Pid);
-
- bw.Write(attachTimeout);
- bw.Write(profilerGuid.ToByteArray());
- bw.WriteString(profilerPath);
+ writer.Write(attachTimeout);
+ writer.Write(profilerGuid.ToByteArray());
+ writer.WriteString(profilerPath);
if (additionalData == null)
{
- bw.Write(0);
+ writer.Write(0);
}
else
{
- bw.Write(additionalData.Length);
- bw.Write(additionalData);
+ writer.Write(additionalData.Length);
+ writer.Write(additionalData);
}
- bw.Flush();
- stream.Position = 0;
-
- var bytes = new byte[stream.Length];
- stream.Read(bytes, 0, bytes.Length);
- return bytes;
+ writer.Flush();
+ return stream.ToArray();
}
}
- private static byte[] SerializeCoreDump(MessageHeader header, Stream stream, string dumpName, DumpType dumpType, bool diagnostics)
+ private static byte[] SerializeCoreDump(string dumpName, DumpType dumpType, bool diagnostics)
{
- using (var bw = new BinaryWriter(stream))
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
{
- bw.Write((uint)header.RequestType);
- bw.Write(header.Pid);
-
- bw.WriteString(dumpName);
- bw.Write((int)dumpType);
- bw.Write(diagnostics ? 1 : 0);
-
- bw.Flush();
- stream.Position = 0;
+ writer.WriteString(dumpName);
+ writer.Write((uint)dumpType);
+ writer.Write((uint)(diagnostics ? 1 : 0));
- var bytes = new byte[stream.Length];
- stream.Read(bytes, 0, bytes.Length);
- return bytes;
+ writer.Flush();
+ return stream.ToArray();
}
}
}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Pipes;
+using System.Linq;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.Diagnostics.Tools.RuntimeClient.DiagnosticsIpc
+{
+ public class IpcClient
+ {
+ private static string DiagnosticPortPattern { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"^dotnetcore-diagnostic-(\d+)$" : @"^dotnetcore-diagnostic-(\d+)-(\d+)-socket$";
+
+ private static string IpcRootPath { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"\\.\pipe\" : Path.GetTempPath();
+
+ private static double ConnectTimeoutMilliseconds { get; } = TimeSpan.FromSeconds(3).TotalMilliseconds;
+
+ /// <summary>
+ /// Get the OS Transport to be used for communicating with a dotnet process.
+ /// </summary>
+ /// <param name="processId">The PID of the dotnet process to get the transport for</param>
+ /// <returns>A System.IO.Stream wrapper around the transport</returns>
+ private static Stream GetTransport(int processId)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ string pipeName = $"dotnetcore-diagnostic-{processId}";
+ var namedPipe = new NamedPipeClientStream(
+ ".", pipeName, PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.Impersonation);
+ namedPipe.Connect((int)ConnectTimeoutMilliseconds);
+ return namedPipe;
+ }
+ else
+ {
+ string ipcPort = Directory.GetFiles(IpcRootPath) // Try best match.
+ .Select(namedPipe => (new FileInfo(namedPipe)).Name)
+ .SingleOrDefault(input => Regex.IsMatch(input, $"^dotnetcore-diagnostic-{processId}-(\\d+)-socket$"));
+ if (ipcPort == null)
+ {
+ throw new PlatformNotSupportedException($"Process {processId} not running compatible .NET Core runtime");
+ }
+ string path = Path.Combine(Path.GetTempPath(), ipcPort);
+ var remoteEP = new UnixDomainSocketEndPoint(path);
+
+ var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
+ socket.Connect(remoteEP);
+ return new NetworkStream(socket);
+ }
+ }
+
+ /// <summary>
+ /// Sends a single DiagnosticIpc Message to the dotnet process with PID processId.
+ /// </summary>
+ /// <param name="processId">The PID of the dotnet process</param>
+ /// <param name="message">The DiagnosticsIpc Message to be sent</param>
+ /// <returns>The response DiagnosticsIpc Message from the dotnet process</returns>
+ public static IpcMessage SendMessage(int processId, IpcMessage message)
+ {
+ using (var stream = GetTransport(processId))
+ {
+ Write(stream, message);
+ return Read(stream);
+ }
+ }
+
+ /// <summary>
+ /// Sends a single DiagnosticIpc Message to the dotnet process with PID processId
+ /// and returns the Stream for reuse in Optional Continuations.
+ /// </summary>
+ /// <param name="processId">The PID of the dotnet process</param>
+ /// <param name="message">The DiagnosticsIpc Message to be sent</param>
+ /// <param name="response">out var for response message</param>
+ /// <returns>The response DiagnosticsIpc Message from the dotnet process</returns>
+ public static Stream SendMessage(int processId, IpcMessage message, out IpcMessage response)
+ {
+ var stream = GetTransport(processId);
+ Write(stream, message);
+ response = Read(stream);
+ return stream;
+ }
+
+ private static void Write(Stream stream, byte[] buffer)
+ {
+ using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
+ {
+ writer.Write(buffer);
+ }
+ }
+
+ private static void Write(Stream stream, IpcMessage message)
+ {
+ using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
+ {
+ writer.Write(message.Serialize());
+ }
+ }
+
+
+ private static IpcMessage Read(Stream stream)
+ {
+ return IpcMessage.Parse(stream);
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.Diagnostics.Tools.RuntimeClient.DiagnosticsIpc
+{
+ public enum DiagnosticServerCommandSet : byte
+ {
+ Dump = 0x01,
+ EventPipe = 0x02,
+ Profiler = 0x03,
+
+ Server = 0xFF,
+ }
+
+ public enum DiagnosticServerCommandId : byte
+ {
+ OK = 0x00,
+ Error = 0xFF,
+ }
+
+ public enum EventPipeCommandId : byte
+ {
+ StopTracing = 0x01,
+ CollectTracing = 0x02,
+ }
+
+ public enum DumpCommandId : byte
+ {
+ GenerateCoreDump = 0x01,
+ }
+
+ public enum ProfilerCommandId : byte
+ {
+ AttachProfiler = 0x01,
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Microsoft.Diagnostics.Tools.RuntimeClient.DiagnosticsIpc
+{
+ public class IpcHeader
+ {
+ IpcHeader() { }
+
+ public IpcHeader(DiagnosticServerCommandSet commandSet, byte commandId)
+ {
+ CommandSet = (byte)commandSet;
+ CommandId = commandId;
+ }
+
+ // the number of bytes for the DiagnosticsIpc::IpcHeader type in native code
+ public static readonly UInt16 HeaderSizeInBytes = 20;
+ private static readonly UInt16 MagicSizeInBytes = 14;
+
+ public byte[] Magic = ASCIIEncoding.ASCII.GetBytes("DOTNET_IPC_V1" + '\0'); // byte[14] in native code
+ public UInt16 Size = HeaderSizeInBytes;
+ public byte CommandSet;
+ public byte CommandId;
+ public UInt16 Reserved = 0x0000;
+
+
+ // Helper expression to quickly get V1 magic string for comparison
+ // should be 14 bytes long
+ public static byte[] DOTNET_IPC_V1 => ASCIIEncoding.ASCII.GetBytes("DOTNET_IPC_V1" + '\0');
+
+ public byte[] Serialize()
+ {
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
+ {
+ writer.Write(Magic);
+ Debug.Assert(Magic.Length == MagicSizeInBytes);
+ writer.Write(Size);
+ writer.Write(CommandSet);
+ writer.Write(CommandId);
+ writer.Write((UInt16)0x0000);
+ writer.Flush();
+ return stream.ToArray();
+ }
+ }
+
+ public static IpcHeader TryParse(BinaryReader reader)
+ {
+ IpcHeader header = new IpcHeader
+ {
+ Magic = reader.ReadBytes(14),
+ Size = reader.ReadUInt16(),
+ CommandSet = reader.ReadByte(),
+ CommandId = reader.ReadByte(),
+ Reserved = reader.ReadUInt16()
+ };
+
+ // TODO: Validate it is correct!
+
+ return header;
+ }
+
+ override public string ToString()
+ {
+ return $"{{ Magic={Magic}; Size={Size}; CommandSet={CommandSet}; CommandId={CommandId}; Reserved={Reserved} }}";
+ }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Microsoft.Diagnostics.Tools.RuntimeClient.DiagnosticsIpc
+{
+ public class IpcMessage
+ {
+ public IpcMessage()
+ { }
+
+ public IpcMessage(IpcHeader header, byte[] payload)
+ {
+ Payload = payload;
+ Header = header;
+ }
+
+ internal IpcMessage(DiagnosticServerCommandSet commandSet, byte commandId, byte[] payload = null)
+ {
+ Header = new IpcHeader(commandSet, commandId);
+ Payload = payload;
+ }
+
+ public byte[] Payload { get; private set; } = null;
+ public IpcHeader Header { get; private set; } = default;
+
+ public byte[] Serialize()
+ {
+ byte[] serializedData = null;
+ // Verify things will fit in the size capacity
+ Header.Size = checked((UInt16)(IpcHeader.HeaderSizeInBytes + Payload.Length)); ;
+ byte[] headerBytes = Header.Serialize();
+
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
+ {
+ writer.Write(headerBytes);
+ writer.Write(Payload);
+ writer.Flush();
+ serializedData = stream.ToArray();
+ }
+
+ return serializedData;
+ }
+
+ public static IpcMessage Parse(Stream stream)
+ {
+ IpcMessage message = new IpcMessage();
+ using (var reader = new BinaryReader(stream, Encoding.UTF8, true))
+ {
+ message.Header = IpcHeader.TryParse(reader);
+ message.Payload = reader.ReadBytes(message.Header.Size - IpcHeader.HeaderSizeInBytes);
+ return message;
+ }
+ }
+ }
+}
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using Microsoft.Diagnostics.Tools.RuntimeClient.DiagnosticsIpc;
using System;
using System.Collections.Generic;
using System.Diagnostics;
private static string IpcRootPath { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"\\.\pipe\" : Path.GetTempPath();
- private static double ConnectTimeoutMilliseconds { get; } = TimeSpan.FromSeconds(3).TotalMilliseconds;
-
- /// <summary>
- /// Send event pipe command.
- /// </summary>
- /// <param name="processId">runtime process id</param>
- /// <param name="buffer">serialized command</param>
- /// <returns>command result</returns>
- public static ulong SendCommand(int processId, byte[] buffer)
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- string pipeName = $"dotnetcore-diagnostic-{processId}";
- using (var namedPipe = new NamedPipeClientStream(
- ".", pipeName, PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.Impersonation))
- {
- namedPipe.Connect((int)ConnectTimeoutMilliseconds);
- namedPipe.Write(buffer, 0, buffer.Length);
-
- return new BinaryReader(namedPipe).ReadUInt64();
- }
- }
- else
- {
- string ipcPort = Directory.GetFiles(IpcRootPath) // Try best match.
- .Select(namedPipe => (new FileInfo(namedPipe)).Name)
- .SingleOrDefault(input => Regex.IsMatch(input, $"^dotnetcore-diagnostic-{processId}-(\\d+)-socket$"));
- if (ipcPort == null)
- {
- throw new PlatformNotSupportedException($"Process {processId} not running compatible .NET Core runtime");
- }
- string path = Path.Combine(Path.GetTempPath(), ipcPort);
- var remoteEP = new UnixDomainSocketEndPoint(path);
-
- using (var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified))
- {
- socket.Connect(remoteEP);
- socket.Send(buffer);
-
- var content = new byte[sizeof(ulong)];
- int nReceivedBytes = socket.Receive(content);
- return (nReceivedBytes == sizeof(ulong)) ? BitConverter.ToUInt64(content, 0) : 0;
- }
- }
- }
-
/// <summary>
/// Get the files associated with the opened IPC Ports for DotNet Core applications.
/// </summary>
public static Stream CollectTracing(int processId, SessionConfiguration configuration, out ulong sessionId)
{
sessionId = 0;
+ var message = new IpcMessage(DiagnosticServerCommandSet.EventPipe, (byte)EventPipeCommandId.CollectTracing, configuration.Serialize());
+ var stream = IpcClient.SendMessage(processId, message, out var response);
- var header = new MessageHeader {
- RequestType = DiagnosticMessageType.CollectEventPipeTracing,
- Pid = (uint)Process.GetCurrentProcess().Id,
- };
-
- byte[] serializedConfiguration;
- using (var stream = new MemoryStream())
- serializedConfiguration = Serialize(header, configuration, stream);
-
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- string pipeName = $"dotnetcore-diagnostic-{processId}";
- var namedPipe = new NamedPipeClientStream(
- ".", pipeName, PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.Impersonation);
- namedPipe.Connect((int)ConnectTimeoutMilliseconds);
-
- // Request start-collection
- namedPipe.Write(serializedConfiguration, 0, serializedConfiguration.Length);
-
- sessionId = new BinaryReader(namedPipe).ReadUInt64();
- return namedPipe;
- }
- else
+ switch ((DiagnosticServerCommandId)response.Header.CommandId)
{
- // TODO: Determine ApplicationGroupId
- string ipcPort = Directory.GetFiles(IpcRootPath) // Try best match.
- .Select(namedPipe => (new FileInfo(namedPipe)).Name)
- .SingleOrDefault(input => Regex.IsMatch(input, $"^dotnetcore-diagnostic-{processId}-(\\d+)-socket$"));
- if (ipcPort == null)
- {
- throw new PlatformNotSupportedException($"Process {processId} not running compatible .NET Core runtime");
- }
- string path = Path.Combine(Path.GetTempPath(), ipcPort);
- var remoteEP = new UnixDomainSocketEndPoint(path);
-
- var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
- socket.Connect(remoteEP);
-
- // Request start-collection
- socket.Send(serializedConfiguration);
-
- var content = new byte[sizeof(ulong)];
- int nReceivedBytes = socket.Receive(content);
- sessionId = (nReceivedBytes == sizeof(ulong)) ? BitConverter.ToUInt64(content, 0) : 0;
-
- return new NetworkStream(socket, FileAccess.Read, true);
+ case DiagnosticServerCommandId.OK:
+ sessionId = BitConverter.ToUInt64(response.Payload);
+ break;
+ case DiagnosticServerCommandId.Error:
+ // bad...
+ var hr = BitConverter.ToInt32(response.Payload);
+ throw new Exception($"Session start FAILED 0x{hr:X8}");
+ default:
+ break;
}
- }
- /// <summary>
- /// Start tracing to file.
- /// </summary>
- /// <param name="processId">Runtime process to trace</param>
- /// <param name="configuration">buffer size, file path and provider configuration</param>
- /// <returns>session id</returns>
- public static ulong StartTracingToFile(int processId, SessionConfiguration configuration)
- {
- var header = new MessageHeader {
- RequestType = DiagnosticMessageType.StartEventPipeTracing,
- Pid = (uint)Process.GetCurrentProcess().Id,
- };
-
- byte[] serializedConfiguration;
- using (var stream = new MemoryStream())
- serializedConfiguration = Serialize(header, configuration, stream);
-
- return SendCommand(processId, serializedConfiguration);
+ return stream;
}
/// <summary>
if (sessionId == 0)
return sessionId; // TODO: Throw here instead?
- var header = new MessageHeader {
- RequestType = DiagnosticMessageType.StopEventPipeTracing,
- Pid = (uint)Process.GetCurrentProcess().Id,
- };
+ byte[] payload = BitConverter.GetBytes(sessionId);
- byte[] sessionIdInBytes;
- using (var stream = new MemoryStream())
- {
- using (var sw = new BinaryWriter(stream))
- {
- sw.Write((uint)header.RequestType);
- sw.Write(header.Pid);
+ var response = IpcClient.SendMessage(processId, new IpcMessage(DiagnosticServerCommandSet.EventPipe, (byte)EventPipeCommandId.StopTracing, payload));
- sw.Write(sessionId);
- sw.Flush();
- sessionIdInBytes = stream.ToArray();
- }
- }
-
- return SendCommand(processId, sessionIdInBytes);
- }
-
- private static byte[] Serialize(MessageHeader header, SessionConfiguration configuration, Stream stream)
- {
- using (var bw = new BinaryWriter(stream))
+ switch ((DiagnosticServerCommandId)response.Header.CommandId)
{
- bw.Write((uint)header.RequestType);
- bw.Write(header.Pid);
-
- bw.Write(configuration.CircularBufferSizeInMB);
-
- bw.WriteString(configuration.OutputPath);
-
- bw.Write(configuration.Providers.Count());
- foreach (var provider in configuration.Providers)
- {
- bw.Write(provider.Keywords);
- bw.Write((uint)provider.EventLevel);
-
- bw.WriteString(provider.Name);
- bw.WriteString(provider.FilterData);
- }
-
- bw.Flush();
- stream.Position = 0;
-
- var bytes = new byte[stream.Length];
- stream.Read(bytes, 0, bytes.Length);
- return bytes;
+ case DiagnosticServerCommandId.OK:
+ return BitConverter.ToUInt64(response.Payload);
+ case DiagnosticServerCommandId.Error:
+ return 0;
+ default:
+ return 0;
}
}
}
private readonly FileInfo _outputPath;
private readonly List<Provider> _providers;
+
+ public byte[] Serialize()
+ {
+ byte[] serializedData = null;
+ using (var stream = new MemoryStream())
+ using (var writer = new BinaryWriter(stream))
+ {
+ writer.Write(CircularBufferSizeInMB);
+
+ writer.WriteString(OutputPath);
+
+ writer.Write(Providers.Count());
+ foreach (var provider in Providers)
+ {
+ writer.Write(provider.Keywords);
+ writer.Write((uint)provider.EventLevel);
+
+ writer.WriteString(provider.Name);
+ writer.WriteString(provider.FilterData);
+ }
+
+ writer.Flush();
+ serializedData = stream.ToArray();
+ }
+
+ return serializedData;
+ }
}
}
DiagnosticHelpers.DumpType dumpType = type == DumpTypeOption.Heap ? DiagnosticHelpers.DumpType.WithHeap : DiagnosticHelpers.DumpType.Normal;
// Send the command to the runtime to initiate the core dump
- int hr = DiagnosticHelpers.GenerateCoreDump(processId, output, dumpType, diagnostics: false);
+ var hr = DiagnosticHelpers.GenerateCoreDump(processId, output, dumpType, diagnostics: false);
if (hr != 0)
{
throw new InvalidOperationException($"Core dump generation FAILED 0x{hr:X8}");