-
Notifications
You must be signed in to change notification settings - Fork 358
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Microsoft.Diagnostics.NETCore.Client Implementation #617
Changes from 17 commits
22030df
3fde2a7
d9efaff
13c8306
f8667ff
54b78a6
fa48772
9c8dc4d
614ba31
142e938
e22532d
45886f1
4dec226
ccd1c17
46f0402
31aaa2a
3b51ff0
f087d15
0642cad
a0b3c57
1a47b41
48333bd
bec51df
5151b30
d3cf367
6fc7fb3
488e137
3697a54
272a459
81e446d
dba6940
bba974e
810e67d
21465f5
ac5c16d
48892d4
44e1163
17eff82
22613bf
233a8b5
adc565c
968574d
c61bcf9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Globalization; | ||
using System.Linq; | ||
using System.Runtime.InteropServices; | ||
using System.Text.RegularExpressions; | ||
|
||
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 class DiagnosticsClient | ||
{ | ||
private int _processId; | ||
private static string DiagnosticsPortPattern { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"^dotnet-diagnostic-(\d+)$" : @"^dotnet-diagnostic-(\d+)-(\d+)-socket$"; | ||
private static string IpcRootPath { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"\\.\pipe\" : Path.GetTempPath(); | ||
sywhang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public DiagnosticsClient(int processId) | ||
{ | ||
_processId = processId; | ||
} | ||
|
||
/// <summary> | ||
/// Start tracing the application via CollectTracing2 command. | ||
/// </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=256) | ||
{ | ||
return new EventPipeSession(_processId, providers, requestRundown, circularBufferMB); | ||
} | ||
|
||
/// <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) | ||
{ | ||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) | ||
throw new PlatformNotSupportedException($"Unsupported operating system: {RuntimeInformation.OSDescription}"); | ||
|
||
if (string.IsNullOrEmpty(dumpPath)) | ||
throw new ArgumentNullException($"{nameof(dumpPath)} required"); | ||
|
||
var payload = SerializeCoreDump(dumpPath, dumpType, logDumpGeneration); | ||
var message = new IpcMessage(DiagnosticsServerCommandSet.Dump, (byte)DumpCommandId.GenerateCoreDump, payload); | ||
var response = IpcClient.SendMessage(_processId, message); | ||
var hr = 0; | ||
switch ((DiagnosticsServerCommandId)response.Header.CommandId) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: This |
||
{ | ||
case DiagnosticsServerCommandId.Error: | ||
throw new ServerErrorException($"Writing dump failed (HRESULT: 0x{hr:X8})"); | ||
case DiagnosticsServerCommandId.OK: | ||
hr = BitConverter.ToInt32(response.Payload, 0); | ||
sywhang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
break; | ||
default: | ||
throw new ServerErrorException($"Writing dump failed - server responded with unknown command"); | ||
} | ||
return; | ||
} | ||
|
||
/// <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) | ||
{ | ||
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"); | ||
} | ||
|
||
var header = new MessageHeader { | ||
sywhang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
RequestType = DiagnosticsMessageType.AttachProfiler, | ||
Pid = (uint)Process.GetCurrentProcess().Id, | ||
}; | ||
|
||
byte[] serializedConfiguration = SerializeProfilerAttach((uint)attachTimeout.TotalSeconds, profilerGuid, profilerPath, additionalData); | ||
var message = new IpcMessage(DiagnosticsServerCommandSet.Profiler, (byte)ProfilerCommandId.AttachProfiler, serializedConfiguration); | ||
var response = IpcClient.SendMessage(_processId, message); | ||
switch ((DiagnosticsServerCommandId)response.Header.CommandId) | ||
{ | ||
case DiagnosticsServerCommandId.Error: | ||
var hr = BitConverter.ToInt32(response.Payload, 0); | ||
throw new ServerErrorException($"Profiler attach failed (HRESULT: 0x{hr:X8})"); | ||
case DiagnosticsServerCommandId.OK: | ||
return; | ||
default: | ||
throw new ServerErrorException($"Profiler attach failed - server responded with unknown command"); | ||
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. | ||
sywhang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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; | ||
} | ||
|
||
/// <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() | ||
{ | ||
return Directory.GetFiles(IpcRootPath) | ||
.Select(namedPipe => (new FileInfo(namedPipe)).Name) | ||
.Where(input => Regex.IsMatch(input, DiagnosticsPortPattern)) | ||
.Select(input => int.Parse(Regex.Match(input, DiagnosticsPortPattern).Groups[1].Value, NumberStyles.Integer)); | ||
} | ||
|
||
private static byte[] SerializeCoreDump(string dumpName, DumpType dumpType, bool diagnostics) | ||
{ | ||
using (var stream = new MemoryStream()) | ||
using (var writer = new BinaryWriter(stream)) | ||
{ | ||
writer.WriteString(dumpName); | ||
writer.Write((uint)dumpType); | ||
writer.Write((uint)(diagnostics ? 1 : 0)); | ||
|
||
writer.Flush(); | ||
return stream.ToArray(); | ||
} | ||
} | ||
|
||
private static byte[] SerializeProfilerAttach(uint attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData) | ||
{ | ||
using (var stream = new MemoryStream()) | ||
using (var writer = new BinaryWriter(stream)) | ||
{ | ||
writer.Write(attachTimeout); | ||
writer.Write(profilerGuid.ToByteArray()); | ||
writer.WriteString(profilerPath); | ||
|
||
if (additionalData == null) | ||
{ | ||
writer.Write(0); | ||
} | ||
else | ||
{ | ||
writer.Write(additionalData.Length); | ||
writer.Write(additionalData); | ||
} | ||
|
||
writer.Flush(); | ||
return stream.ToArray(); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
|
||
namespace Microsoft.Diagnostics.NETCore.Client | ||
{ | ||
public class DiagnosticsClientException : Exception {} | ||
|
||
// When a certian command is not supported by either the library or the target process' runtime | ||
public class UnsupportedProtocolException : DiagnosticsClientException {} | ||
|
||
// When the runtime is no longer availble for attaching. | ||
public class ServerNotAvailableException : DiagnosticsClientException {} | ||
|
||
// When the runtime responded with an error | ||
public class ServerErrorException : DiagnosticsClientException {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
namespace Microsoft.Diagnostics.NETCore.Client | ||
{ | ||
public enum DumpType | ||
{ | ||
Normal = 1, | ||
WithHeap = 2, | ||
Triage = 3, | ||
Full = 4 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics.Tracing; | ||
using System.Text; | ||
|
||
namespace Microsoft.Diagnostics.NETCore.Client | ||
{ | ||
public class EventPipeProvider | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
{ | ||
public EventPipeProvider(string name, EventLevel eventLevel, long keywords = 0, IDictionary<string, string> arguments = null) | ||
sywhang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
Name = name; | ||
EventLevel = eventLevel; | ||
Keywords = keywords; | ||
Arguments = arguments; | ||
} | ||
|
||
public long Keywords { get; } | ||
|
||
public EventLevel EventLevel { get; } | ||
|
||
public string Name { get; } | ||
|
||
public IDictionary<string, string> Arguments { get; } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: The object seems to be immutable, could use |
||
|
||
public override string ToString() | ||
{ | ||
return $"{Name}:0x{Keywords:X16}:{(uint)EventLevel}{(Arguments == null ? "" : $":{GetArgumentString()}")}"; | ||
} | ||
|
||
public override bool Equals(object obj) | ||
sywhang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (obj == null || GetType() != obj.GetType()) | ||
{ | ||
return false; | ||
} | ||
|
||
return this == (EventPipeProvider)obj; | ||
} | ||
|
||
public override int GetHashCode() | ||
{ | ||
int hash = 0; | ||
hash ^= this.Name.GetHashCode(); | ||
hash ^= this.Keywords.GetHashCode(); | ||
hash ^= this.EventLevel.GetHashCode(); | ||
hash ^= GetArgumentString().GetHashCode(); | ||
return hash; | ||
} | ||
|
||
public static bool operator ==(EventPipeProvider left, EventPipeProvider right) | ||
{ | ||
return left.ToString() == right.ToString(); | ||
sywhang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
public static bool operator !=(EventPipeProvider left, EventPipeProvider right) | ||
{ | ||
return !(left == right); | ||
} | ||
|
||
internal string GetArgumentString() | ||
{ | ||
if (Arguments == null) | ||
{ | ||
return ""; | ||
} | ||
StringBuilder sb = new StringBuilder(); | ||
sywhang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
foreach(var argument in Arguments) | ||
{ | ||
sb.Append($"{argument.Key}={argument.Value};"); | ||
} | ||
return sb.ToString(0, sb.Length-1); | ||
} | ||
|
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I do not see a
virtual
method in this class (no plan on allowing inheritance), therefore I would make itsealed
.