From 8565c145933459c1e64e218dbe88a5c7b5fe1d59 Mon Sep 17 00:00:00 2001 From: Sung Yoon Whang Date: Tue, 17 Dec 2019 12:32:48 -0800 Subject: [PATCH] Push Microsoft.Diagnostics.NETCore.Client to release branch (#705) * Microsoft.Diagnostics.NETCore.Client Implementation (#617) This is the initial implementation of Microsoft.Diagnostics.NETCore.Client library. This will be released with version 1.0.0-preview, and when I feel that enough customers have validated it that we can match it with the rest of the diagnostics OOB packages and bump up the version to 3.1. * add .NET Standard 2.0 for diagnostics client library (#700) * Make MultiplePublishedProcessTest more stable (#701) * Make MultiplePublishedProcessTest more stable * Bump up DiagnosticsClient library version to 0.2.0 (#704) --- diagnostics.sln | 129 ++++++++++++++ .../BinaryWriterExtensions.cs | 25 +++ .../DiagnosticsClient/DiagnosticsClient.cs | 163 ++++++++++++++++++ .../DiagnosticsClientExceptions.cs | 31 ++++ .../DiagnosticsClient/DumpType.cs | 14 ++ .../DiagnosticsClient/EventPipeProvider.cs | 76 ++++++++ .../DiagnosticsClient/EventPipeSession.cs | 85 +++++++++ .../EventPipeSessionConfiguration.cs | 72 ++++++++ .../DiagnosticsIpc/IpcClient.cs | 136 +++++++++++++++ .../DiagnosticsIpc/IpcCommands.cs | 42 +++++ .../DiagnosticsIpc/IpcHeader.cs | 75 ++++++++ .../DiagnosticsIpc/IpcMessage.cs | 108 ++++++++++++ ...icrosoft.Diagnostics.NETCore.Client.csproj | 15 ++ src/Tools/Common/Commands/ProcessStatus.cs | 4 +- src/Tools/dotnet-counters/CounterMonitor.cs | 54 ++---- src/Tools/dotnet-counters/Program.cs | 2 +- .../dotnet-counters/dotnet-counters.csproj | 2 +- src/Tools/dotnet-dump/Dumper.cs | 14 +- src/Tools/dotnet-dump/Program.cs | 2 +- src/Tools/dotnet-dump/dotnet-dump.csproj | 2 +- .../CommandLine/CollectCommandHandler.cs | 1 - .../ProcessStatusCommandHandler.cs | 1 - .../EventPipeDotNetHeapDumper.cs | 39 ++--- src/Tools/dotnet-gcdump/dotnet-gcdump.csproj | 2 +- .../CommandLine/Commands/CollectCommand.cs | 34 ++-- .../CommandLine/Commands/ConvertCommand.cs | 2 +- .../Commands/ListProcessesCommandHandler.cs | 1 - .../Commands/ListProfilesCommandHandler.cs | 32 ++-- .../Commands/StopCommandHandler.cs | 48 ------ src/Tools/dotnet-trace/Extensions.cs | 64 ++++++- src/Tools/dotnet-trace/Profile.cs | 16 +- src/Tools/dotnet-trace/Program.cs | 3 - src/Tools/dotnet-trace/dotnet-trace.csproj | 2 +- .../CommonHelper.cs | 28 +++ .../EventPipeProviderTests.cs | 113 ++++++++++++ .../EventPipeSessionTests.cs | 115 ++++++++++++ .../GetPublishedProcessesTests.cs | 76 ++++++++ ...iagnostics.NETCore.Client.UnitTests.csproj | 13 ++ .../TestRunner.cs | 86 +++++++++ .../WriteDumpTests.cs | 124 +++++++++++++ src/tests/Tracee/Program.cs | 21 +++ src/tests/Tracee/Tracee.csproj | 7 + .../dotnet-trace/ProfileProviderMerging.cs | 8 +- src/tests/dotnet-trace/ProviderParsing.cs | 81 +++++---- 44 files changed, 1749 insertions(+), 219 deletions(-) create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/BinaryWriterExtensions.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClientExceptions.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DumpType.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeProvider.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSession.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSessionConfiguration.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcClient.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcHeader.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs create mode 100644 src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj delete mode 100644 src/Tools/dotnet-trace/CommandLine/Commands/StopCommandHandler.cs create mode 100644 src/tests/Microsoft.Diagnostics.NETCore.Client/CommonHelper.cs create mode 100644 src/tests/Microsoft.Diagnostics.NETCore.Client/EventPipeProviderTests.cs create mode 100644 src/tests/Microsoft.Diagnostics.NETCore.Client/EventPipeSessionTests.cs create mode 100644 src/tests/Microsoft.Diagnostics.NETCore.Client/GetPublishedProcessesTests.cs create mode 100644 src/tests/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.UnitTests.csproj create mode 100644 src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs create mode 100644 src/tests/Microsoft.Diagnostics.NETCore.Client/WriteDumpTests.cs create mode 100644 src/tests/Tracee/Program.cs create mode 100644 src/tests/Tracee/Tracee.csproj diff --git a/diagnostics.sln b/diagnostics.sln index 6796cda207..45df2c9e38 100644 --- a/diagnostics.sln +++ b/diagnostics.sln @@ -55,6 +55,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-gcdump", "src\Tools\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetCounters.UnitTests", "src\tests\dotnet-counters\DotnetCounters.UnitTests.csproj", "{E5A7DC6C-BF8D-418A-BCBD-094EB748FA82}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.NETCore.Client", "src\Microsoft.Diagnostics.NETCore.Client\Microsoft.Diagnostics.NETCore.Client.csproj", "{D8BE9C81-194E-43E5-82CF-2080892FBDE7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.NETCore.Client.UnitTests", "src\tests\Microsoft.Diagnostics.NETCore.Client\Microsoft.Diagnostics.NETCore.Client.UnitTests.csproj", "{6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tracee", "src\tests\Tracee\Tracee.csproj", "{C79D6069-2C18-48CB-846E-71F7168C2F7D}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventPipe.UnitTests", "src\tests\eventpipe\EventPipe.UnitTests.csproj", "{CED9ABBA-861E-4C0A-9359-22351208EF27}" EndProject Global @@ -890,6 +896,126 @@ Global {E5A7DC6C-BF8D-418A-BCBD-094EB748FA82}.RelWithDebInfo|x64.Build.0 = Release|Any CPU {E5A7DC6C-BF8D-418A-BCBD-094EB748FA82}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU {E5A7DC6C-BF8D-418A-BCBD-094EB748FA82}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Checked|Any CPU.Build.0 = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Checked|ARM.ActiveCfg = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Checked|ARM.Build.0 = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Checked|ARM64.ActiveCfg = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Checked|ARM64.Build.0 = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Checked|x64.ActiveCfg = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Checked|x64.Build.0 = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Checked|x86.ActiveCfg = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Checked|x86.Build.0 = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Debug|ARM.ActiveCfg = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Debug|ARM.Build.0 = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Debug|ARM64.Build.0 = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Debug|x64.Build.0 = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Debug|x86.ActiveCfg = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Debug|x86.Build.0 = Debug|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Release|Any CPU.Build.0 = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Release|ARM.ActiveCfg = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Release|ARM.Build.0 = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Release|ARM64.ActiveCfg = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Release|ARM64.Build.0 = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Release|x64.ActiveCfg = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Release|x64.Build.0 = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Release|x86.ActiveCfg = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.Release|x86.Build.0 = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {D8BE9C81-194E-43E5-82CF-2080892FBDE7}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Checked|Any CPU.Build.0 = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Checked|ARM.ActiveCfg = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Checked|ARM.Build.0 = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Checked|ARM64.ActiveCfg = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Checked|ARM64.Build.0 = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Checked|x64.ActiveCfg = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Checked|x64.Build.0 = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Checked|x86.ActiveCfg = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Checked|x86.Build.0 = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Debug|ARM.ActiveCfg = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Debug|ARM.Build.0 = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Debug|ARM64.Build.0 = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Debug|x64.ActiveCfg = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Debug|x64.Build.0 = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Debug|x86.ActiveCfg = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Debug|x86.Build.0 = Debug|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Release|Any CPU.Build.0 = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Release|ARM.ActiveCfg = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Release|ARM.Build.0 = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Release|ARM64.ActiveCfg = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Release|ARM64.Build.0 = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Release|x64.ActiveCfg = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Release|x64.Build.0 = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Release|x86.ActiveCfg = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.Release|x86.Build.0 = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Checked|Any CPU.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Checked|ARM.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Checked|ARM.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Checked|ARM64.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Checked|ARM64.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Checked|x64.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Checked|x64.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Checked|x86.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Checked|x86.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Debug|ARM.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Debug|ARM64.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Debug|x64.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Debug|x86.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Release|Any CPU.Build.0 = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Release|ARM.ActiveCfg = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Release|ARM.Build.0 = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Release|ARM64.ActiveCfg = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Release|ARM64.Build.0 = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Release|x64.ActiveCfg = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Release|x64.Build.0 = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Release|x86.ActiveCfg = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.Release|x86.Build.0 = Release|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.RelWithDebInfo|Any CPU.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.RelWithDebInfo|Any CPU.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.RelWithDebInfo|ARM.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.RelWithDebInfo|ARM.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.RelWithDebInfo|ARM64.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.RelWithDebInfo|ARM64.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.RelWithDebInfo|x64.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.RelWithDebInfo|x64.Build.0 = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.RelWithDebInfo|x86.ActiveCfg = Debug|Any CPU + {C79D6069-2C18-48CB-846E-71F7168C2F7D}.RelWithDebInfo|x86.Build.0 = Debug|Any CPU {CED9ABBA-861E-4C0A-9359-22351208EF27}.Checked|Any CPU.ActiveCfg = Debug|Any CPU {CED9ABBA-861E-4C0A-9359-22351208EF27}.Checked|Any CPU.Build.0 = Debug|Any CPU {CED9ABBA-861E-4C0A-9359-22351208EF27}.Checked|ARM.ActiveCfg = Debug|Any CPU @@ -960,6 +1086,9 @@ Global {AEDCCF5B-5AD0-4D64-BF73-5CF468E07D22} = {03479E19-3F18-49A6-910A-F5041E27E7C0} {936678B3-3392-4F4F-943C-B6A4BFCBAADC} = {B62728C8-1267-4043-B46F-5537BBAEC692} {E5A7DC6C-BF8D-418A-BCBD-094EB748FA82} = {03479E19-3F18-49A6-910A-F5041E27E7C0} + {D8BE9C81-194E-43E5-82CF-2080892FBDE7} = {19FAB78C-3351-4911-8F0C-8C6056401740} + {6F4DD2F8-1C7B-4A87-B7E5-1BEE9F5AC128} = {03479E19-3F18-49A6-910A-F5041E27E7C0} + {C79D6069-2C18-48CB-846E-71F7168C2F7D} = {03479E19-3F18-49A6-910A-F5041E27E7C0} {CED9ABBA-861E-4C0A-9359-22351208EF27} = {03479E19-3F18-49A6-910A-F5041E27E7C0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/Microsoft.Diagnostics.NETCore.Client/BinaryWriterExtensions.cs b/src/Microsoft.Diagnostics.NETCore.Client/BinaryWriterExtensions.cs new file mode 100644 index 0000000000..c95b24d5e3 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/BinaryWriterExtensions.cs @@ -0,0 +1,25 @@ +// 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.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal static class BinaryWriterExtensions + { + public static void WriteString(this BinaryWriter @this, string value) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + @this.Write(value != null ? (value.Length + 1) : 0); + if (value != null) + @this.Write(Encoding.Unicode.GetBytes(value + '\0')); + } + + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs new file mode 100644 index 0000000000..5d558c4ff9 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs @@ -0,0 +1,163 @@ +// 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 +{ + /// + /// This is a top-level class that contains methods to send various diagnostics command to the runtime. + /// + public sealed class DiagnosticsClient + { + private int _processId; + + public DiagnosticsClient(int processId) + { + _processId = processId; + } + + /// + /// Start tracing the application and return an EventPipeSession object + /// + /// An IEnumerable containing the list of Providers to turn on. + /// If true, request rundown events from the runtime + /// The size of the runtime's buffer for collecting events in MB + /// + /// An EventPipeSession object representing the EventPipe session that just started. + /// + public EventPipeSession StartEventPipeSession(IEnumerable providers, bool requestRundown=true, int circularBufferMB=256) + { + return new EventPipeSession(_processId, providers, requestRundown, circularBufferMB); + } + + /// + /// Trigger a core dump generation. + /// + /// Type of the dump to be generated + /// Full path to the dump to be generated. By default it is /tmp/coredump.{pid} + /// When set to true, display the dump generation debug log to the console. + 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) + { + case DiagnosticsServerCommandId.Error: + hr = BitConverter.ToInt32(response.Payload, 0); + throw new ServerErrorException($"Writing dump failed (HRESULT: 0x{hr:X8})"); + case DiagnosticsServerCommandId.OK: + return; + default: + throw new ServerErrorException($"Writing dump failed - server responded with unknown command"); + } + } + + /// + /// Attach a profiler. + /// + /// Timeout for attaching the profiler + /// Guid for the profiler to be attached + /// Path to the profiler to be attached + /// Additional data to be passed to the profiler + 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"); + } + + 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"); + } + + // 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. + } + + /// + /// Get all the active processes that can be attached to. + /// + /// + /// IEnumerable of all the active process IDs. + /// + public static IEnumerable GetPublishedProcesses() + { + return Directory.GetFiles(IpcClient.IpcRootPath) + .Select(namedPipe => (new FileInfo(namedPipe)).Name) + .Where(input => Regex.IsMatch(input, IpcClient.DiagnosticsPortPattern)) + .Select(input => int.Parse(Regex.Match(input, IpcClient.DiagnosticsPortPattern).Groups[1].Value, NumberStyles.Integer)) + .Distinct(); + } + + 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(); + } + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClientExceptions.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClientExceptions.cs new file mode 100644 index 0000000000..a313e3c533 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClientExceptions.cs @@ -0,0 +1,31 @@ +// 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 + { + public DiagnosticsClientException(string msg) : base(msg) {} + } + + // When a certian command is not supported by either the library or the target process' runtime + public class UnsupportedProtocolException : DiagnosticsClientException + { + public UnsupportedProtocolException(string msg) : base(msg) {} + } + + // When the runtime is no longer availble for attaching. + public class ServerNotAvailableException : DiagnosticsClientException + { + public ServerNotAvailableException(string msg) : base(msg) {} + } + + // When the runtime responded with an error + public class ServerErrorException : DiagnosticsClientException + { + public ServerErrorException(string msg): base(msg) {} + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DumpType.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DumpType.cs new file mode 100644 index 0000000000..2200e84fb2 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DumpType.cs @@ -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 + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeProvider.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeProvider.cs new file mode 100644 index 0000000000..332ecd20a3 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeProvider.cs @@ -0,0 +1,76 @@ +// 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.Linq; +using System.Text; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + public sealed class EventPipeProvider + { + public EventPipeProvider(string name, EventLevel eventLevel, long keywords = 0, IDictionary arguments = null) + { + Name = name; + EventLevel = eventLevel; + Keywords = keywords; + Arguments = arguments; + } + + public long Keywords { get; } + + public EventLevel EventLevel { get; } + + public string Name { get; } + + public IDictionary Arguments { get; } + + public override string ToString() + { + return $"{Name}:0x{Keywords:X16}:{(uint)EventLevel}{(Arguments == null ? "" : $":{GetArgumentString()}")}"; + } + + public override bool Equals(object obj) + { + 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(); + } + + public static bool operator !=(EventPipeProvider left, EventPipeProvider right) + { + return !(left == right); + } + + internal string GetArgumentString() + { + if (Arguments == null) + { + return ""; + } + return string.Join(";", Arguments.Select(a => $"{a.Key}={a.Value}")); + } + + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSession.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSession.cs new file mode 100644 index 0000000000..fc6fa847d4 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSession.cs @@ -0,0 +1,85 @@ +// 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; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + public class EventPipeSession : IDisposable + { + private IEnumerable _providers; + private bool _requestRundown; + private int _circularBufferMB; + private long _sessionId; + private int _processId; + private bool disposedValue = false; // To detect redundant calls + + internal EventPipeSession(int processId, IEnumerable providers, bool requestRundown, int circularBufferMB) + { + _processId = processId; + _providers = providers; + _requestRundown = requestRundown; + _circularBufferMB = circularBufferMB; + + var config = new EventPipeSessionConfiguration(circularBufferMB, EventPipeSerializationFormat.NetTrace, providers, requestRundown); + var message = new IpcMessage(DiagnosticsServerCommandSet.EventPipe, (byte)EventPipeCommandId.CollectTracing2, config.SerializeV2()); + EventStream = IpcClient.SendMessage(processId, message, out var response); + switch ((DiagnosticsServerCommandId)response.Header.CommandId) + { + case DiagnosticsServerCommandId.OK: + _sessionId = BitConverter.ToInt64(response.Payload, 0); + break; + case DiagnosticsServerCommandId.Error: + var hr = BitConverter.ToInt32(response.Payload, 0); + throw new ServerErrorException($"EventPipe session start failed (HRESULT: 0x{hr:X8})"); + default: + throw new ServerErrorException($"EventPipe session start failed - Server responded with unknown command"); + } + } + + public Stream EventStream { get; } + + /// + /// Stops the given session + /// + public void Stop() + { + Debug.Assert(_sessionId > 0); + + byte[] payload = BitConverter.GetBytes(_sessionId); + var response = IpcClient.SendMessage(_processId, new IpcMessage(DiagnosticsServerCommandSet.EventPipe, (byte)EventPipeCommandId.StopTracing, payload)); + + switch ((DiagnosticsServerCommandId)response.Header.CommandId) + { + case DiagnosticsServerCommandId.OK: + return; + case DiagnosticsServerCommandId.Error: + var hr = BitConverter.ToInt32(response.Payload, 0); + throw new ServerErrorException($"EventPipe session stop failed (HRESULT: 0x{hr:X8})"); + default: + throw new ServerErrorException($"EventPipe session stop failed - Server responded with unknown command"); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + EventStream?.Dispose(); + } + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSessionConfiguration.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSessionConfiguration.cs new file mode 100644 index 0000000000..abda64566a --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/EventPipeSessionConfiguration.cs @@ -0,0 +1,72 @@ +// 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.IO; +using System.Linq; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal enum EventPipeSerializationFormat + { + NetPerf, + NetTrace + } + + internal class EventPipeSessionConfiguration + { + public EventPipeSessionConfiguration(int circularBufferSizeMB, EventPipeSerializationFormat format, IEnumerable providers, bool requestRundown=true) + { + if (circularBufferSizeMB == 0) + throw new ArgumentException($"Buffer size cannot be zero."); + if (format != EventPipeSerializationFormat.NetPerf && format != EventPipeSerializationFormat.NetTrace) + throw new ArgumentException("Unrecognized format"); + if (providers == null) + throw new ArgumentNullException(nameof(providers)); + + CircularBufferSizeInMB = circularBufferSizeMB; + Format = format; + RequestRundown = requestRundown; + _providers = new List(providers); + } + + public bool RequestRundown { get; } + public int CircularBufferSizeInMB { get; } + public EventPipeSerializationFormat Format { get; } + + public IReadOnlyCollection Providers => _providers.AsReadOnly(); + + private readonly List _providers; + + public byte[] SerializeV2() + { + byte[] serializedData = null; + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + { + writer.Write(CircularBufferSizeInMB); + writer.Write((uint)Format); + writer.Write(RequestRundown); + + 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.GetArgumentString()); + } + + writer.Flush(); + serializedData = stream.ToArray(); + } + + return serializedData; + } + + + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcClient.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcClient.cs new file mode 100644 index 0000000000..1ab6409ea6 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcClient.cs @@ -0,0 +1,136 @@ +// 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.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal class IpcClient + { + public static string IpcRootPath { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"\\.\pipe\" : Path.GetTempPath(); + public static string DiagnosticsPortPattern { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"^dotnet-diagnostic-(\d+)$" : @"^dotnet-diagnostic-(\d+)-(\d+)-socket$"; + + private static double ConnectTimeoutMilliseconds { get; } = TimeSpan.FromSeconds(3).TotalMilliseconds; + + /// + /// Get the OS Transport to be used for communicating with a dotnet process. + /// + /// The PID of the dotnet process to get the transport for + /// A System.IO.Stream wrapper around the transport + private static Stream GetTransport(int processId) + { + try + { + var process = Process.GetProcessById(processId); + } + catch (System.ArgumentException) + { + throw new ServerNotAvailableException($"Process {processId} is not running."); + } + catch (System.InvalidOperationException) + { + throw new ServerNotAvailableException($"Process {processId} seems to be elevated."); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string pipeName = $"dotnet-diagnostic-{processId}"; + var namedPipe = new NamedPipeClientStream( + ".", pipeName, PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.Impersonation); + namedPipe.Connect((int)ConnectTimeoutMilliseconds); + return namedPipe; + } + else + { + string ipcPort; + try + { + ipcPort = Directory.GetFiles(IpcRootPath, $"dotnet-diagnostic-{processId}-*-socket") // Try best match. + .OrderByDescending(f => new FileInfo(f).LastWriteTime) + .FirstOrDefault(); + if (ipcPort == null) + { + throw new ServerNotAvailableException($"Process {processId} not running compatible .NET Core runtime."); + } + } + catch (InvalidOperationException) + { + throw new ServerNotAvailableException($"Process {processId} not running compatible .NET Core runtime."); + } + string path = Path.Combine(IpcRootPath, ipcPort); + var remoteEP = CreateUnixDomainSocketEndPoint(path); + + var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + socket.Connect(remoteEP); + return new NetworkStream(socket); + } + } + + /// + /// Sends a single DiagnosticsIpc Message to the dotnet process with PID processId. + /// + /// The PID of the dotnet process + /// The DiagnosticsIpc Message to be sent + /// The response DiagnosticsIpc Message from the dotnet process + public static IpcMessage SendMessage(int processId, IpcMessage message) + { + using (var stream = GetTransport(processId)) + { + Write(stream, message); + return Read(stream); + } + } + + /// + /// Sends a single DiagnosticsIpc Message to the dotnet process with PID processId + /// and returns the Stream for reuse in Optional Continuations. + /// + /// The PID of the dotnet process + /// The DiagnosticsIpc Message to be sent + /// out var for response message + /// The response DiagnosticsIpc Message from the dotnet process + 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) + { + stream.Write(buffer, 0, buffer.Length); + } + + private static void Write(Stream stream, IpcMessage message) + { + Write(stream, message.Serialize()); + } + + private static IpcMessage Read(Stream stream) + { + return IpcMessage.Parse(stream); + } + + private static EndPoint CreateUnixDomainSocketEndPoint(string path) + { +#if NETCOREAPP + return new UnixDomainSocketEndPoint(path); +#elif NETSTANDARD2_0 + // UnixDomainSocketEndPoint is not part of .NET Standard 2.0 + var type = typeof(Socket).Assembly.GetType("System.Net.Sockets.UnixDomainSocketEndPoint"); + var ctor = type.GetConstructor(new[] { typeof(string) }); + return (EndPoint)ctor.Invoke(new object[] { path }); +#endif + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs new file mode 100644 index 0000000000..7c870e3c1f --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs @@ -0,0 +1,42 @@ +// 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.Text; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal enum DiagnosticsServerCommandSet : byte + { + Dump = 0x01, + EventPipe = 0x02, + Profiler = 0x03, + + Server = 0xFF, + } + + internal enum DiagnosticsServerCommandId : byte + { + OK = 0x00, + Error = 0xFF, + } + + internal enum EventPipeCommandId : byte + { + StopTracing = 0x01, + CollectTracing = 0x02, + CollectTracing2 = 0x03, + } + + internal enum DumpCommandId : byte + { + GenerateCoreDump = 0x01, + } + + internal enum ProfilerCommandId : byte + { + AttachProfiler = 0x01, + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcHeader.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcHeader.cs new file mode 100644 index 0000000000..3a8fe59287 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcHeader.cs @@ -0,0 +1,75 @@ +// 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.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal class IpcHeader + { + IpcHeader() { } + + public IpcHeader(DiagnosticsServerCommandSet 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 = DotnetIpcV1; // 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[] DotnetIpcV1 => Encoding.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() + }; + + return header; + } + + override public string ToString() + { + return $"{{ Magic={Magic}; Size={Size}; CommandSet={CommandSet}; CommandId={CommandId}; Reserved={Reserved} }}"; + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs new file mode 100644 index 0000000000..06cd79df25 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs @@ -0,0 +1,108 @@ +// 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.IO; +using System.Text; +using System.Runtime.InteropServices; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + /// + /// Different diagnostic message types that are handled by the runtime. + /// + internal enum DiagnosticsMessageType : uint + { + /// + /// Initiates core dump generation + /// + GenerateCoreDump = 1, + /// + /// Starts an EventPipe session that writes events to a file when the session is stopped or the application exits. + /// + StartEventPipeTracing = 1024, + /// + /// Stops an EventPipe session. + /// + StopEventPipeTracing = 1025, + /// + /// Starts an EventPipe session that sends events out-of-proc through IPC. + /// + CollectEventPipeTracing = 1026, + /// + /// Attaches a profiler to an existing process + /// + AttachProfiler = 2048, + } + + + /// + /// Message header used to send commands to the .NET Core runtime through IPC. + /// + [StructLayout(LayoutKind.Sequential)] + internal struct MessageHeader + { + /// + /// Request type. + /// + public DiagnosticsMessageType RequestType; + + /// + /// Remote process Id. + /// + public uint Pid; + } + + + internal class IpcMessage + { + public IpcMessage() + { } + + public IpcMessage(IpcHeader header, byte[] payload) + { + Payload = payload; + Header = header; + } + + public IpcMessage(DiagnosticsServerCommandSet commandSet, byte commandId, byte[] payload = null) + : this(new IpcHeader(commandSet, commandId), 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; + } + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj new file mode 100644 index 0000000000..7e530dc181 --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj @@ -0,0 +1,15 @@ + + + Library + netstandard2.0;netcoreapp2.1 + Microsoft.Diagnostics.NETCore.Client + .NET Core Diagnostics Client Library + 0.2.0 + true + Diagnostic + $(Description) + true + true + true + + diff --git a/src/Tools/Common/Commands/ProcessStatus.cs b/src/Tools/Common/Commands/ProcessStatus.cs index cdf2111fbd..8fc3ce68a8 100644 --- a/src/Tools/Common/Commands/ProcessStatus.cs +++ b/src/Tools/Common/Commands/ProcessStatus.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Text; -using Microsoft.Diagnostics.Tools.RuntimeClient; +using Microsoft.Diagnostics.NETCore.Client; namespace Microsoft.Internal.Common.Commands { @@ -22,7 +22,7 @@ public static void PrintProcessStatus(IConsole console) try { StringBuilder sb = new StringBuilder(); - var processes = EventPipeClient.ListAvailablePorts() + var processes = DiagnosticsClient.GetPublishedProcesses() .Select(GetProcessById) .Where(process => process != null) .OrderBy(process => process.ProcessName) diff --git a/src/Tools/dotnet-counters/CounterMonitor.cs b/src/Tools/dotnet-counters/CounterMonitor.cs index d4097ecb22..835ee6f7a7 100644 --- a/src/Tools/dotnet-counters/CounterMonitor.cs +++ b/src/Tools/dotnet-counters/CounterMonitor.cs @@ -2,7 +2,7 @@ // 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; +using Microsoft.Diagnostics.NETCore.Client; using System; using System.Collections.Generic; using System.CommandLine; @@ -26,9 +26,10 @@ public class CounterMonitor private IConsole _console; private ICounterRenderer _renderer; private CounterFilter filter; - private ulong _sessionId; private string _output; private bool pauseCmdSet; + private DiagnosticsClient _diagnosticsClient; + private EventPipeSession _session; public CounterMonitor() { @@ -59,7 +60,7 @@ private void StopMonitor() { try { - EventPipeClient.StopTracing(_processId, _sessionId); + _session.Stop(); } catch (EndOfStreamException ex) { @@ -91,6 +92,7 @@ public async Task Monitor(CancellationToken ct, List counter_list, _processId = processId; _interval = refreshInterval; _renderer = new ConsoleWriter(); + _diagnosticsClient = new DiagnosticsClient(processId); return await Start(); } @@ -99,7 +101,7 @@ public async Task Monitor(CancellationToken ct, List counter_list, { try { - EventPipeClient.StopTracing(_processId, _sessionId); + _session.Stop(); } catch (Exception) {} // Swallow all exceptions for now. @@ -118,6 +120,7 @@ public async Task Collect(CancellationToken ct, List counter_list, _processId = processId; _interval = refreshInterval; _output = output; + _diagnosticsClient = new DiagnosticsClient(processId); if (_output.Length == 0) { @@ -154,30 +157,6 @@ public async Task Collect(CancellationToken ct, List counter_list, return 1; } - - // Use EventPipe CollectTracing2 command to start monitoring. This may throw. - private EventPipeEventSource RequestTracingV2(string providerString) - { - var configuration = new SessionConfigurationV2( - circularBufferSizeMB: 1000, - format: EventPipeSerializationFormat.NetTrace, - requestRundown: false, - providers: Trace.Extensions.ToProviders(providerString)); - var binaryReader = EventPipeClient.CollectTracing2(_processId, configuration, out _sessionId); - return new EventPipeEventSource(binaryReader); - } - - // Use EventPipe CollectTracing command to start monitoring. This may throw. - private EventPipeEventSource RequestTracingV1(string providerString) - { - var configuration = new SessionConfiguration( - circularBufferSizeMB: 1000, - format: EventPipeSerializationFormat.NetTrace, - providers: Trace.Extensions.ToProviders(providerString)); - var binaryReader = EventPipeClient.CollectTracing(_processId, configuration, out _sessionId); - return new EventPipeEventSource(binaryReader); - } - private string BuildProviderString() { string providerString; @@ -257,23 +236,16 @@ private async Task Start() Task monitorTask = new Task(() => { try { - EventPipeEventSource source = null; - - try - { - source = RequestTracingV2(providerString); - } - catch (EventPipeUnknownCommandException) - { - // If unknown command exception is thrown, it's likely the app being monitored is - // running an older version of runtime that doesn't support CollectTracingV2. Try again with V1. - source = RequestTracingV1(providerString); - } - + _session = _diagnosticsClient.StartEventPipeSession(Trace.Extensions.ToProviders(providerString), false); + var source = new EventPipeEventSource(_session.EventStream); source.Dynamic.All += DynamicAllMonitor; _renderer.EventPipeSourceConnected(); source.Process(); } + catch (DiagnosticsClientException ex) + { + Console.WriteLine($"Failed to start the counter session: {ex.ToString()}"); + } catch (Exception ex) { Debug.WriteLine($"[ERROR] {ex.ToString()}"); diff --git a/src/Tools/dotnet-counters/Program.cs b/src/Tools/dotnet-counters/Program.cs index 318d40fba9..ca6d20b9f9 100644 --- a/src/Tools/dotnet-counters/Program.cs +++ b/src/Tools/dotnet-counters/Program.cs @@ -11,7 +11,7 @@ using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; -using Microsoft.Diagnostics.Tools.RuntimeClient; +using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Internal.Common.Commands; namespace Microsoft.Diagnostics.Tools.Counters diff --git a/src/Tools/dotnet-counters/dotnet-counters.csproj b/src/Tools/dotnet-counters/dotnet-counters.csproj index b8418707eb..4dc5bed68d 100644 --- a/src/Tools/dotnet-counters/dotnet-counters.csproj +++ b/src/Tools/dotnet-counters/dotnet-counters.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Tools/dotnet-dump/Dumper.cs b/src/Tools/dotnet-dump/Dumper.cs index 5d94f515d0..3f1c37d50e 100644 --- a/src/Tools/dotnet-dump/Dumper.cs +++ b/src/Tools/dotnet-dump/Dumper.cs @@ -2,7 +2,7 @@ // 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; +using Microsoft.Diagnostics.NETCore.Client; using System; using System.CommandLine; using System.Diagnostics; @@ -61,14 +61,11 @@ public async Task Collect(IConsole console, int processId, string output, b } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - DiagnosticsHelpers.DumpType dumpType = type == DumpTypeOption.Heap ? DiagnosticsHelpers.DumpType.WithHeap : DiagnosticsHelpers.DumpType.Normal; + var client = new DiagnosticsClient(processId); + DumpType dumpType = type == DumpTypeOption.Heap ? DumpType.WithHeap : DumpType.Normal; // Send the command to the runtime to initiate the core dump - var hr = DiagnosticsHelpers.GenerateCoreDump(processId, output, dumpType, diag); - if (hr != 0) - { - throw new InvalidOperationException($"Core dump generation FAILED 0x{hr:X8}"); - } + client.WriteDump(dumpType, output, diag); } else { throw new PlatformNotSupportedException($"Unsupported operating system: {RuntimeInformation.OSDescription}"); @@ -81,7 +78,8 @@ ex is UnauthorizedAccessException || ex is PlatformNotSupportedException || ex is InvalidDataException || ex is InvalidOperationException || - ex is NotSupportedException) + ex is NotSupportedException || + ex is DiagnosticsClientException) { console.Error.WriteLine($"{ex.Message}"); return 1; diff --git a/src/Tools/dotnet-dump/Program.cs b/src/Tools/dotnet-dump/Program.cs index 2f2d9aa041..c206bd3365 100644 --- a/src/Tools/dotnet-dump/Program.cs +++ b/src/Tools/dotnet-dump/Program.cs @@ -7,7 +7,7 @@ using System.CommandLine.Invocation; using System.IO; using System.Threading.Tasks; -using Microsoft.Diagnostics.Tools.RuntimeClient; +using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Internal.Common.Commands; namespace Microsoft.Diagnostics.Tools.Dump diff --git a/src/Tools/dotnet-dump/dotnet-dump.csproj b/src/Tools/dotnet-dump/dotnet-dump.csproj index c6ba3724d2..d93281273e 100644 --- a/src/Tools/dotnet-dump/dotnet-dump.csproj +++ b/src/Tools/dotnet-dump/dotnet-dump.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs b/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs index 5ad9bd9245..4be21afb8e 100644 --- a/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs +++ b/src/Tools/dotnet-gcdump/CommandLine/CollectCommandHandler.cs @@ -2,7 +2,6 @@ // 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; using System; using System.Collections.Generic; using System.CommandLine; diff --git a/src/Tools/dotnet-gcdump/CommandLine/ProcessStatusCommandHandler.cs b/src/Tools/dotnet-gcdump/CommandLine/ProcessStatusCommandHandler.cs index 056be27294..4e15083f0c 100644 --- a/src/Tools/dotnet-gcdump/CommandLine/ProcessStatusCommandHandler.cs +++ b/src/Tools/dotnet-gcdump/CommandLine/ProcessStatusCommandHandler.cs @@ -2,7 +2,6 @@ // 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; using Microsoft.Internal.Common.Commands; using System; using System.CommandLine; diff --git a/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs b/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs index 440a2b2e51..511bb448f2 100644 --- a/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs +++ b/src/Tools/dotnet-gcdump/DotNetHeapDump/EventPipeDotNetHeapDumper.cs @@ -3,13 +3,14 @@ // See the LICENSE file in the project root for more information. using Graphs; -using Microsoft.Diagnostics.Tools.RuntimeClient; +using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Diagnostics.Tracing; using Microsoft.Diagnostics.Tracing.Parsers; using Microsoft.Diagnostics.Tracing.Parsers.Clr; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.Tracing; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -48,7 +49,9 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processID, Memory bool fDone = false; log.WriteLine("{0,5:n1}s: Creating type table flushing task", getElapsed().TotalSeconds); - using (EventPipeSession typeFlushSession = new EventPipeSession(processID, new List { new Provider("Microsoft-DotNETCore-SampleProfiler") }, false)) + using (EventPipeSessionController typeFlushSession = new EventPipeSessionController(processID, new List { + new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational) + }, false)) { log.WriteLine("{0,5:n1}s: Flushing the type table", getElapsed().TotalSeconds); typeFlushSession.Source.AllEvents += (traceEvent) => { @@ -68,7 +71,9 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processID, Memory // Start the providers and trigger the GCs. log.WriteLine("{0,5:n1}s: Requesting a .NET Heap Dump", getElapsed().TotalSeconds); - using EventPipeSession gcDumpSession = new EventPipeSession(processID, new List { new Provider("Microsoft-Windows-DotNETRuntime", (ulong)(ClrTraceEventParser.Keywords.GCHeapSnapshot)) }); + using EventPipeSessionController gcDumpSession = new EventPipeSessionController(processID, new List { + new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, (long)(ClrTraceEventParser.Keywords.GCHeapSnapshot)) + }); log.WriteLine("{0,5:n1}s: gcdump EventPipe Session started", getElapsed().TotalSeconds); int gcNum = -1; @@ -211,35 +216,29 @@ public static bool DumpFromEventPipe(CancellationToken ct, int processID, Memory } } - internal class EventPipeSession : IDisposable + internal class EventPipeSessionController : IDisposable { - private List _providers; - private Stream _eventPipeStream; + private List _providers; + private DiagnosticsClient _client; + private EventPipeSession _session; private EventPipeEventSource _source; - private ulong _sessionId; private int _pid; - public ulong SessionId => _sessionId; - public IReadOnlyList Providers => _providers.AsReadOnly(); + public IReadOnlyList Providers => _providers.AsReadOnly(); public EventPipeEventSource Source => _source; - public EventPipeSession(int pid, List providers, bool requestRundown = true) + public EventPipeSessionController(int pid, List providers, bool requestRundown = true) { _pid = pid; _providers = providers; - var config = new SessionConfigurationV2( - circularBufferSizeMB: 1024, - format: EventPipeSerializationFormat.NetTrace, - requestRundown: requestRundown, - providers - ); - _eventPipeStream = EventPipeClient.CollectTracing2(pid, config, out _sessionId); - _source = new EventPipeEventSource(_eventPipeStream); + _client = new DiagnosticsClient(pid); + _session = _client.StartEventPipeSession(providers, requestRundown, 1024); + _source = new EventPipeEventSource(_session.EventStream); } public void EndSession() { - EventPipeClient.StopTracing(_pid, _sessionId); + _session.Stop(); } #region IDisposable Support @@ -251,7 +250,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - _eventPipeStream?.Dispose(); + _session?.Dispose(); _source?.Dispose(); } disposedValue = true; diff --git a/src/Tools/dotnet-gcdump/dotnet-gcdump.csproj b/src/Tools/dotnet-gcdump/dotnet-gcdump.csproj index 6c09dc9963..11d02c0a2a 100644 --- a/src/Tools/dotnet-gcdump/dotnet-gcdump.csproj +++ b/src/Tools/dotnet-gcdump/dotnet-gcdump.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index fac9c13b1a..b8c156c411 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -2,7 +2,7 @@ // 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; +using Microsoft.Diagnostics.NETCore.Client; using System; using System.Collections.Generic; using System.CommandLine; @@ -59,7 +59,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i Dictionary enabledBy = new Dictionary(); var providerCollection = Extensions.ToProviders(providers); - foreach (Provider providerCollectionProvider in providerCollection) + foreach (EventPipeProvider providerCollectionProvider in providerCollection) { enabledBy[providerCollectionProvider.Name] = "--providers "; } @@ -87,11 +87,6 @@ private static async Task Collect(CancellationToken ct, IConsole console, i PrintProviders(providerCollection, enabledBy); var process = Process.GetProcessById(processId); - var configuration = new SessionConfiguration( - circularBufferSizeMB: buffersize, - format: EventPipeSerializationFormat.NetTrace, - providers: providerCollection); - var shouldExit = new ManualResetEvent(false); var shouldStopAfterDuration = duration != default(TimeSpan); var failed = false; @@ -100,11 +95,20 @@ private static async Task Collect(CancellationToken ct, IConsole console, i ct.Register(() => shouldExit.Set()); - ulong sessionId = 0; - using (Stream stream = EventPipeClient.CollectTracing(processId, configuration, out sessionId)) + var diagnosticsClient = new DiagnosticsClient(processId); using (VirtualTerminalMode vTermMode = VirtualTerminalMode.TryEnable()) { - if (sessionId == 0) + EventPipeSession session = null; + try + { + session = diagnosticsClient.StartEventPipeSession(providerCollection, true); + } + catch (DiagnosticsClientException e) + { + Console.Error.WriteLine($"Unable to start a tracing session: {e.ToString()}"); + } + + if (session == null) { Console.Error.WriteLine("Unable to create session."); return ErrorCodes.SessionCreationError; @@ -137,7 +141,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i while (true) { - int nBytesRead = stream.Read(buffer, 0, buffer.Length); + int nBytesRead = session.EventStream.Read(buffer, 0, buffer.Length); if (nBytesRead <= 0) break; fs.Write(buffer, 0, nBytesRead); @@ -170,7 +174,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i if (!terminated) { durationTimer?.Stop(); - EventPipeClient.StopTracing(processId, sessionId); + session.Stop(); } await collectingTask; } @@ -190,7 +194,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i } } - private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy) + private static void PrintProviders(IReadOnlyList providers, Dictionary enabledBy) { Console.Out.WriteLine(""); Console.Out.Write(String.Format("{0, -40}","Provider Name")); // +4 is for the tab @@ -199,10 +203,12 @@ private static void PrintProviders(IReadOnlyList providers, Dictionary Console.Out.Write("Enabled By\n"); foreach (var provider in providers) { - Console.Out.WriteLine(String.Format("{0, -80}", $"{provider.ToDisplayString()}") + $"{enabledBy[provider.Name]}"); + Console.Out.WriteLine(String.Format("{0, -80}", $"{GetProviderDisplayString(provider)}") + $"{enabledBy[provider.Name]}"); } Console.Out.WriteLine(); } + private static string GetProviderDisplayString(EventPipeProvider provider) => + String.Format("{0, -40}", provider.Name) + String.Format("0x{0, -18}", $"{provider.Keywords:X16}") + String.Format("{0, -8}", provider.EventLevel.ToString() + $"({(int)provider.EventLevel})"); private static int prevBufferWidth = 0; private static string clearLineString = ""; diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs index cddfb89089..ced5842d4b 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ConvertCommand.cs @@ -2,7 +2,7 @@ // 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; +using Microsoft.Diagnostics.NETCore.Client; using System; using System.IO; using System.CommandLine; diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ListProcessesCommandHandler.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ListProcessesCommandHandler.cs index c8afc5695d..c30ec69404 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ListProcessesCommandHandler.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ListProcessesCommandHandler.cs @@ -2,7 +2,6 @@ // 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; using Microsoft.Internal.Common.Commands; using System; using System.CommandLine; diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs index f8ec9089fd..a182ae3f4a 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/ListProfilesCommandHandler.cs @@ -2,7 +2,7 @@ // 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; +using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Diagnostics.Tracing.Parsers; using System; using System.Collections.Generic; @@ -39,35 +39,35 @@ public static Command ListProfilesCommand() => handler: CommandHandler.Create(GetProfiles), isHidden: false); - // FIXME: Read from a config file! internal static IEnumerable DotNETRuntimeProfiles { get; } = new[] { new Profile( "cpu-sampling", - new Provider[] { - new Provider("Microsoft-DotNETCore-SampleProfiler"), - new Provider("Microsoft-Windows-DotNETRuntime", (ulong)ClrTraceEventParser.Keywords.Default, EventLevel.Informational), + new EventPipeProvider[] { + new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational), + new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, (long)ClrTraceEventParser.Keywords.Default) }, "Useful for tracking CPU usage and general .NET runtime information. This is the default option if no profile or providers are specified."), new Profile( "gc-verbose", - new Provider[] { - new Provider( + new EventPipeProvider[] { + new EventPipeProvider( name: "Microsoft-Windows-DotNETRuntime", - keywords: (ulong)ClrTraceEventParser.Keywords.GC | - (ulong)ClrTraceEventParser.Keywords.GCHandle | - (ulong)ClrTraceEventParser.Keywords.Exception, - eventLevel: EventLevel.Verbose + eventLevel: EventLevel.Verbose, + keywords: (long)ClrTraceEventParser.Keywords.GC | + (long)ClrTraceEventParser.Keywords.GCHandle | + (long)ClrTraceEventParser.Keywords.Exception ), }, "Tracks GC collections and samples object allocations."), new Profile( "gc-collect", - new Provider[] { - new Provider( + new EventPipeProvider[] { + new EventPipeProvider( name: "Microsoft-Windows-DotNETRuntime", - keywords: (ulong)ClrTraceEventParser.Keywords.GC | - (ulong)ClrTraceEventParser.Keywords.Exception, - eventLevel: EventLevel.Informational), + eventLevel: EventLevel.Informational, + keywords: (long)ClrTraceEventParser.Keywords.GC | + (long)ClrTraceEventParser.Keywords.Exception + ) }, "Tracks GC collections only at very low overhead."), }; diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/StopCommandHandler.cs b/src/Tools/dotnet-trace/CommandLine/Commands/StopCommandHandler.cs deleted file mode 100644 index 073ffb4058..0000000000 --- a/src/Tools/dotnet-trace/CommandLine/Commands/StopCommandHandler.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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 Microsoft.Diagnostics.Tools.RuntimeClient; -using System; -using System.CommandLine; -using System.CommandLine.Invocation; -using System.Threading.Tasks; - -namespace Microsoft.Diagnostics.Tools.Trace -{ - internal static class StopCommandHandler - { - public static async Task Stop(IConsole console, int processId, ulong sessionId) - { - try - { - EventPipeClient.StopTracing(processId, sessionId); - - await Task.FromResult(0); - return sessionId != 0 ? 0 : 1; - } - catch (Exception ex) - { - Console.Error.WriteLine($"[ERROR] {ex.ToString()}"); - return 1; - } - } - - public static Command StopCommand() => - new Command( - name: "stop", - description: "Stops an EventPipe session.", - symbols: new Option[] { - CommonOptions.ProcessIdOption(), - SessionIdOption(), - }, - handler: CommandHandler.Create(Stop), - isHidden: true); - - private static Option SessionIdOption() => - new Option( - new[] { "--session-id" }, - @"Session Id being recorded.", - new Argument { Name = "SessionId" }); - } -} diff --git a/src/Tools/dotnet-trace/Extensions.cs b/src/Tools/dotnet-trace/Extensions.cs index a1055eeeb3..59b54664ec 100644 --- a/src/Tools/dotnet-trace/Extensions.cs +++ b/src/Tools/dotnet-trace/Extensions.cs @@ -2,7 +2,7 @@ // 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; +using Microsoft.Diagnostics.NETCore.Client; using System; using System.Collections.Generic; using System.Diagnostics.Tracing; @@ -14,12 +14,12 @@ internal static class Extensions { private static EventLevel defaultEventLevel = EventLevel.Verbose; - public static List ToProviders(string providers) + public static List ToProviders(string providers) { if (providers == null) throw new ArgumentNullException(nameof(providers)); return string.IsNullOrWhiteSpace(providers) ? - new List() : providers.Split(',').Select(ToProvider).ToList(); + new List() : providers.Split(',').Select(ToProvider).ToList(); } private static EventLevel GetEventLevel(string token) @@ -51,7 +51,7 @@ private static EventLevel GetEventLevel(string token) } } - private static Provider ToProvider(string provider) + private static EventPipeProvider ToProvider(string provider) { if (string.IsNullOrWhiteSpace(provider)) throw new ArgumentNullException(nameof(provider)); @@ -71,8 +71,8 @@ private static Provider ToProvider(string provider) throw new ArgumentException("Provider name was not specified."); // Keywords - ulong keywords = tokens.Length > 1 && !string.IsNullOrWhiteSpace(tokens[1]) ? - Convert.ToUInt64(tokens[1], 16) : ulong.MaxValue; + long keywords = tokens.Length > 1 && !string.IsNullOrWhiteSpace(tokens[1]) ? + Convert.ToInt64(tokens[1], 16) : -1; // Level EventLevel eventLevel = tokens.Length > 2 && !string.IsNullOrWhiteSpace(tokens[2]) ? @@ -80,9 +80,57 @@ private static Provider ToProvider(string provider) // Event counters string filterData = tokens.Length > 3 ? tokens[3] : null; - filterData = string.IsNullOrWhiteSpace(filterData) ? null : filterData; + var argument = string.IsNullOrWhiteSpace(filterData) ? null : ParseArgumentString(filterData); + return new EventPipeProvider(providerName, eventLevel, keywords, argument); + } + + private static Dictionary ParseArgumentString(string argument) + { + if (argument == "") + { + return null; + } + var argumentDict = new Dictionary(); - return new Provider(providerName, keywords, eventLevel, filterData); + int keyStart = 0; + int keyEnd = 0; + int valStart = 0; + int valEnd = 0; + int curIdx = 0; + bool inQuote = false; + foreach (var c in argument) + { + if (inQuote) + { + if (c == '\"') + { + inQuote = false; + } + } + else + { + if (c == '=') + { + keyEnd = curIdx; + valStart = curIdx+1; + } + else if (c == ';') + { + valEnd = curIdx; + argumentDict.Add(argument.Substring(keyStart, keyEnd-keyStart), argument.Substring(valStart, valEnd-valStart)); + keyStart = curIdx+1; // new key starts + } + else if (c == '\"') + { + inQuote = true; + } + } + curIdx += 1; + } + string key = argument.Substring(keyStart, keyEnd - keyStart); + string val = argument.Substring(valStart); + argumentDict.Add(key, val); + return argumentDict; } } } diff --git a/src/Tools/dotnet-trace/Profile.cs b/src/Tools/dotnet-trace/Profile.cs index 7bc28c0887..078ef9d854 100644 --- a/src/Tools/dotnet-trace/Profile.cs +++ b/src/Tools/dotnet-trace/Profile.cs @@ -2,7 +2,7 @@ // 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; +using Microsoft.Diagnostics.NETCore.Client; using System.Collections.Generic; using System.Linq; @@ -10,32 +10,32 @@ namespace Microsoft.Diagnostics.Tools.Trace { internal sealed class Profile { - public Profile(string name, IEnumerable providers, string description) + public Profile(string name, IEnumerable providers, string description) { Name = name; - Providers = providers == null ? Enumerable.Empty() : new List(providers).AsReadOnly(); + Providers = providers == null ? Enumerable.Empty() : new List(providers).AsReadOnly(); Description = description; } public string Name { get; } - public IEnumerable Providers { get; } + public IEnumerable Providers { get; } public string Description { get; } - public static void MergeProfileAndProviders(Profile selectedProfile, List providerCollection, Dictionary enabledBy) + public static void MergeProfileAndProviders(Profile selectedProfile, List providerCollection, Dictionary enabledBy) { - var profileProviders = new List(); + var profileProviders = new List(); // If user defined a different key/level on the same provider via --providers option that was specified via --profile option, // --providers option takes precedence. Go through the list of providers specified and only add it if it wasn't specified // via --providers options. if (selectedProfile.Providers != null) { - foreach (Provider selectedProfileProvider in selectedProfile.Providers) + foreach (EventPipeProvider selectedProfileProvider in selectedProfile.Providers) { bool shouldAdd = true; - foreach (Provider providerCollectionProvider in providerCollection) + foreach (EventPipeProvider providerCollectionProvider in providerCollection) { if (providerCollectionProvider.Name.Equals(selectedProfileProvider.Name)) { diff --git a/src/Tools/dotnet-trace/Program.cs b/src/Tools/dotnet-trace/Program.cs index 48d00fb327..606f7ae62c 100644 --- a/src/Tools/dotnet-trace/Program.cs +++ b/src/Tools/dotnet-trace/Program.cs @@ -13,9 +13,6 @@ class Program public static Task Main(string[] args) { var parser = new CommandLineBuilder() -#if DEBUG - .AddCommand(StopCommandHandler.StopCommand()) -#endif .AddCommand(CollectCommandHandler.CollectCommand()) .AddCommand(ListProcessesCommandHandler.ListProcessesCommand()) .AddCommand(ListProfilesCommandHandler.ListProfilesCommand()) diff --git a/src/Tools/dotnet-trace/dotnet-trace.csproj b/src/Tools/dotnet-trace/dotnet-trace.csproj index 43dfc2dc2a..2f7e371333 100644 --- a/src/Tools/dotnet-trace/dotnet-trace.csproj +++ b/src/Tools/dotnet-trace/dotnet-trace.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/CommonHelper.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/CommonHelper.cs new file mode 100644 index 0000000000..d1109110fb --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/CommonHelper.cs @@ -0,0 +1,28 @@ +// 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.IO; +using System.Reflection; +using System.Runtime.InteropServices; + +using Microsoft.Diagnostics.TestHelpers; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + public class CommonHelper + { + public static string HostExe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + "..\\..\\..\\..\\..\\.dotnet\\dotnet.exe" : "../../../../../.dotnet/dotnet"; + + public static string GetTraceePath() + { + var curPath = Directory.GetCurrentDirectory(); +; + var traceePath = curPath.Replace("Microsoft.Diagnostics.NETCore.Client.UnitTests", "Tracee"); + + return Path.Combine(traceePath, "Tracee.dll"); + } + } +} \ No newline at end of file diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/EventPipeProviderTests.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/EventPipeProviderTests.cs new file mode 100644 index 0000000000..5f66f28e32 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/EventPipeProviderTests.cs @@ -0,0 +1,113 @@ +// 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 Microsoft.Diagnostics.NETCore.Client; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Tracing; +using Xunit; + + +namespace Microsoft.Diagnostics.NETCore.Client +{ + + /// + /// Suite of tests that test top-level commands + /// + public class EventPipeProviderTests + { + [Fact] + public void EqualTest1() + { + EventPipeProvider provider1 = new EventPipeProvider("myProvider", EventLevel.Informational); + EventPipeProvider provider2 = new EventPipeProvider("myProvider", EventLevel.Informational); + Assert.True(provider1 == provider2); + } + + [Fact] + public void EqualTest2() + { + EventPipeProvider provider1 = new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, (long)(-1)); + EventPipeProvider provider2 = new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Verbose, (long)(-1)); + Assert.True(provider1 == provider2); + } + + [Fact] + public void EqualTest3() + { + EventPipeProvider provider1 = new EventPipeProvider( + "System.Runtime", + EventLevel.Verbose, + (long)(-1), + new Dictionary() { + { "EventCounterIntervalSec", "1" } + }); + EventPipeProvider provider2 = new EventPipeProvider( + "System.Runtime", + EventLevel.Verbose, + (long)(-1), + new Dictionary() { + { "EventCounterIntervalSec", "1" } + }); + Assert.True(provider1 == provider2); + } + + [Fact] + public void InEqualityTest() + { + var providers = new EventPipeProvider[5]; + providers[0] = new EventPipeProvider("myProvider", EventLevel.Informational); + providers[1] = new EventPipeProvider("myProvider", EventLevel.Informational, (long)(-1)); + providers[2] = new EventPipeProvider("myProvider", EventLevel.Verbose, (long)(-1)); + providers[3] = new EventPipeProvider( + "myProvider", + EventLevel.Verbose, + (long)(-1), + new Dictionary() { + { "EventCounterIntervalSec", "1" } + }); + providers[4] = new EventPipeProvider( + "myProvider", + EventLevel.Verbose, + (long)(-1), + new Dictionary() { + { "EventCounterIntervalSec", "2" } + }); + + for (var i = 0; i < providers.Length-1; i++) + { + for (var j = i+1; j < providers.Length; j++) + { + Assert.True(providers[i] != providers[j]); + } + } + } + + [Fact] + public void ToStringTest1() + { + var provider = new EventPipeProvider("MyProvider", EventLevel.Verbose, (long)(0xdeadbeef)); + Assert.Equal("MyProvider:0x00000000DEADBEEF:5", provider.ToString()); + } + + [Fact] + public void ToStringTest2() + { + var provider1 = new EventPipeProvider("MyProvider", EventLevel.Verbose, (long)(0xdeadbeef), + new Dictionary() + { + { "key1", "value1" }, + }); + var provider2 = new EventPipeProvider("MyProvider", EventLevel.Verbose, (long)(0xdeadbeef), + new Dictionary() + { + { "key1", "value1" }, + { "key2", "value2" } + }); + Assert.Equal("MyProvider:0x00000000DEADBEEF:5:key1=value1", provider1.ToString()); + Assert.Equal("MyProvider:0x00000000DEADBEEF:5:key1=value1;key2=value2", provider2.ToString()); + } + } +} diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/EventPipeSessionTests.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/EventPipeSessionTests.cs new file mode 100644 index 0000000000..6344f47491 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/EventPipeSessionTests.cs @@ -0,0 +1,115 @@ +// 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.Diagnostics.Tracing; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.TestHelpers; +using Microsoft.Diagnostics.NETCore.Client; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + public class EventPipeSessionTests + { + private readonly ITestOutputHelper output; + + public EventPipeSessionTests(ITestOutputHelper outputHelper) + { + output = outputHelper; + } + + /// + /// A simple test that checks if we can create an EventPipeSession on a child process + /// + [Fact] + public void BasicEventPipeSessionTest() + { + TestRunner runner = new TestRunner(CommonHelper.GetTraceePath(), output); + runner.Start(3000); + DiagnosticsClient client = new DiagnosticsClient(runner.Pid); + using (var session = client.StartEventPipeSession(new List() + { + new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Informational) + })) + { + Assert.True(session.EventStream != null); + } + runner.Stop(); + } + + /// + /// Checks if we can create an EventPipeSession and can get some expected events out of it. + /// + [Fact] + public void EventPipeSessionStreamTest() + { + TestRunner runner = new TestRunner(CommonHelper.GetTraceePath(), output); + runner.Start(5000); + DiagnosticsClient client = new DiagnosticsClient(runner.Pid); + runner.PrintStatus(); + output.WriteLine($"[{DateTime.Now.ToString()}] Trying to start an EventPipe session on process {runner.Pid}"); + using (var session = client.StartEventPipeSession(new List() + { + new EventPipeProvider("System.Runtime", EventLevel.Informational, 0, new Dictionary() { + { "EventCounterIntervalSec", "1" } + }) + })) + { + var evntCnt = 0; + + Task streamTask = Task.Run(() => { + var source = new EventPipeEventSource(session.EventStream); + source.Dynamic.All += (TraceEvent obj) => { + output.WriteLine("Got an event"); + evntCnt += 1; + }; + try + { + source.Process(); + } + catch (Exception e) + { + Console.WriteLine("Error encountered while processing events"); + Assert.Equal("", e.ToString()); + } + finally + { + runner.Stop(); + } + }); + output.WriteLine("Waiting for stream Task"); + streamTask.Wait(10000); + output.WriteLine("Done waiting for stream Task"); + Assert.True(evntCnt > 0); + } + } + + /// + /// Tries to start an EventPipe session on a non-existent process + /// + [Fact] + public void EventPipeSessionUnavailableTest() + { + List pids = new List(DiagnosticsClient.GetPublishedProcesses()); + int arbitraryPid = 1; + + DiagnosticsClient client = new DiagnosticsClient(arbitraryPid); + + Assert.Throws(() => client.StartEventPipeSession(new List() + { + new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Informational) + })); + } + } +} diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/GetPublishedProcessesTests.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/GetPublishedProcessesTests.cs new file mode 100644 index 0000000000..e46884d65a --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/GetPublishedProcessesTests.cs @@ -0,0 +1,76 @@ +// 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.Threading; +using System.Runtime.InteropServices; +using Xunit; +using Xunit.Abstractions; + +using Microsoft.Diagnostics.TestHelpers; +using Microsoft.Diagnostics.NETCore.Client; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + + /// + /// Suite of tests that test top-level commands + /// + public class GetPublishedProcessesTest + { + private readonly ITestOutputHelper output; + + public GetPublishedProcessesTest(ITestOutputHelper outputHelper) + { + output = outputHelper; + } + + [Fact] + public void PublishedProcessTest1() + { + TestRunner runner = new TestRunner(CommonHelper.GetTraceePath(), output); + runner.Start(3000); + List publishedProcesses = new List(DiagnosticsClient.GetPublishedProcesses()); + foreach (int p in publishedProcesses) + { + output.WriteLine($"[{DateTime.Now.ToString()}] Saw published process {p}"); + } + Assert.Contains(publishedProcesses, p => p == runner.Pid); + runner.Stop(); + } + + [Fact] + public void MultiplePublishedProcessTest() + { + TestRunner[] runner = new TestRunner[3]; + int[] pids = new int[3]; + + for (var i = 0; i < 3; i++) + { + runner[i] = new TestRunner(CommonHelper.GetTraceePath(), output); + runner[i].Start(); + pids[i] = runner[i].Pid; + } + System.Threading.Thread.Sleep(2000); + List publishedProcesses = new List(DiagnosticsClient.GetPublishedProcesses()); + foreach (int p in publishedProcesses) + { + output.WriteLine($"[{DateTime.Now.ToString()}] Saw published process {p}"); + } + + for (var i = 0; i < 3; i++) + { + Assert.Contains(publishedProcesses, p => p == pids[i]); + } + + for (var i = 0 ; i < 3; i++) + { + runner[i].Stop(); + } + } + } +} diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.UnitTests.csproj b/src/tests/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.UnitTests.csproj new file mode 100644 index 0000000000..772f23d5a6 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.UnitTests.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.0 + + + + + + + + + diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs new file mode 100644 index 0000000000..420bde839b --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs @@ -0,0 +1,86 @@ +// 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.Threading; +using Xunit; +using Xunit.Abstractions; + +using Microsoft.Diagnostics.TestHelpers; + +using Microsoft.Diagnostics.NETCore.Client; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + public class TestRunner + { + private Process testProcess; + private ProcessStartInfo startInfo; + private ITestOutputHelper outputHelper; + + public TestRunner(string testExePath, ITestOutputHelper _outputHelper=null) + { + startInfo = new ProcessStartInfo(CommonHelper.HostExe, testExePath); + startInfo.UseShellExecute = false; + startInfo.RedirectStandardOutput = true; + outputHelper = _outputHelper; + } + + public void AddEnvVar(string key, string value) + { + startInfo.EnvironmentVariables[key] = value; + } + + public void Start(int timeoutInMS=0) + { + if (outputHelper != null) + outputHelper.WriteLine("$[{DateTime.Now.ToString()}] Launching test: " + startInfo.FileName); + + testProcess = Process.Start(startInfo); + + if (testProcess == null) + { + outputHelper.WriteLine($"Could not start process: " + startInfo.FileName); + } + + if (testProcess.HasExited) + { + outputHelper.WriteLine($"Process " + startInfo.FileName + " came back as exited"); + } + + if (outputHelper != null) + { + outputHelper.WriteLine($"[{DateTime.Now.ToString()}] Successfuly started process {testProcess.Id}"); + outputHelper.WriteLine($"Have total {testProcess.Modules.Count} modules loaded"); + } + + outputHelper.WriteLine($"[{DateTime.Now.ToString()}] Sleeping for {timeoutInMS} ms."); + Thread.Sleep(timeoutInMS); + outputHelper.WriteLine($"[{DateTime.Now.ToString()}] Done sleeping. Ready to test."); + } + + public void Stop() + { + testProcess.Kill(); + } + + public int Pid { + get { return testProcess.Id; } + } + + public void PrintStatus() + { + if (testProcess.HasExited) + { + outputHelper.WriteLine($"Process {testProcess.Id} status: Exited"); + } + else + { + outputHelper.WriteLine($"Process {testProcess.Id} status: Running"); + } + } + } +} diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/WriteDumpTests.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/WriteDumpTests.cs new file mode 100644 index 0000000000..f438f882c1 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/WriteDumpTests.cs @@ -0,0 +1,124 @@ +// 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.Diagnostics.Tracing; +using System.IO; +using System.Runtime.InteropServices; +using Xunit; +using Xunit.Abstractions; + +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.TestHelpers; +using Microsoft.Diagnostics.NETCore.Client; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + public class WriteDumpTests + { + private readonly ITestOutputHelper output; + + public WriteDumpTests(ITestOutputHelper outputHelper) + { + output = outputHelper; + } + + /// + /// A simple test that writes a single dump file + /// + [Fact] + public void BasicWriteDumpTest() + { + var dumpPath = "./myDump.dmp"; + TestRunner runner = new TestRunner(CommonHelper.GetTraceePath(), output); + runner.Start(3000); + DiagnosticsClient client = new DiagnosticsClient(runner.Pid); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Throws(() => client.WriteDump(DumpType.Normal, dumpPath)); + } + else + { + output.WriteLine($"Requesting dump at {DateTime.Now.ToString()}"); + client.WriteDump(DumpType.Normal, dumpPath); + Assert.True(File.Exists(dumpPath)); + File.Delete(dumpPath); + } + runner.Stop(); + } + + /// + /// A test that writes all the different types of dump file + /// + [Fact] + public void WriteAllDumpTypesTest() + { + var normalDumpPath = "./myDump-normal.dmp"; + var heapDumpPath = "./myDump-heap.dmp"; + var triageDumpPath = "./myDump-triage.dmp"; + var fullDumpPath = "./myDump-full.dmp"; + TestRunner runner = new TestRunner(CommonHelper.GetTraceePath(), output); + runner.Start(3000); + DiagnosticsClient client = new DiagnosticsClient(runner.Pid); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Throws(() => client.WriteDump(DumpType.Normal, normalDumpPath)); + Assert.Throws(() => client.WriteDump(DumpType.WithHeap, heapDumpPath)); + Assert.Throws(() => client.WriteDump(DumpType.Triage, triageDumpPath)); + Assert.Throws(() => client.WriteDump(DumpType.Full, fullDumpPath)); + } + else + { + // Write each type of dump + output.WriteLine($"Requesting dump at {DateTime.Now.ToString()}"); + client.WriteDump(DumpType.Normal, normalDumpPath); + client.WriteDump(DumpType.WithHeap, heapDumpPath); + client.WriteDump(DumpType.Triage, triageDumpPath); + client.WriteDump(DumpType.Full, fullDumpPath); + + // Check they were all created + Assert.True(File.Exists(normalDumpPath)); + Assert.True(File.Exists(heapDumpPath)); + Assert.True(File.Exists(triageDumpPath)); + Assert.True(File.Exists(fullDumpPath)); + + // Remove them + File.Delete(normalDumpPath); + File.Delete(heapDumpPath); + File.Delete(triageDumpPath); + File.Delete(fullDumpPath); + } + runner.Stop(); + } + + /// + /// A test that tries to write a dump of a non-existent process + /// + [Fact] + public void WriteDumpFailTest() + { + List pids = new List(DiagnosticsClient.GetPublishedProcesses()); + int arbitraryPid = 1; + string dumpPath = "./myDump.dmp"; + while (pids.Contains(arbitraryPid)) + { + arbitraryPid += 1; + } + + var client = new DiagnosticsClient(arbitraryPid); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Throws(() => client.WriteDump(DumpType.Normal, dumpPath)); + } + else + { + Assert.Throws(() => client.WriteDump(DumpType.Normal, "./myDump.dmp")); + } + } + } +} diff --git a/src/tests/Tracee/Program.cs b/src/tests/Tracee/Program.cs new file mode 100644 index 0000000000..c6143cf4d1 --- /dev/null +++ b/src/tests/Tracee/Program.cs @@ -0,0 +1,21 @@ +// 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.Threading; + +namespace Tracee +{ + class Program + { + static void Main(string[] args) + { + // Runs for max of 30 sec + for(var i = 0; i < 30; i++) + { + Thread.Sleep(1000); + } + } + } +} diff --git a/src/tests/Tracee/Tracee.csproj b/src/tests/Tracee/Tracee.csproj new file mode 100644 index 0000000000..58b042cb66 --- /dev/null +++ b/src/tests/Tracee/Tracee.csproj @@ -0,0 +1,7 @@ + + + Exe + $(BuildProjectFramework) + netcoreapp3.0; + + diff --git a/src/tests/dotnet-trace/ProfileProviderMerging.cs b/src/tests/dotnet-trace/ProfileProviderMerging.cs index 6cf17b120b..c438728da3 100644 --- a/src/tests/dotnet-trace/ProfileProviderMerging.cs +++ b/src/tests/dotnet-trace/ProfileProviderMerging.cs @@ -1,10 +1,9 @@ // 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 Microsoft.Diagnostics.NETCore.Client; using System; using Xunit; -using Microsoft.Diagnostics.Tools.RuntimeClient; using System.Collections.Generic; using System.Linq; using System.Diagnostics.Tracing; @@ -21,7 +20,7 @@ public void DuplicateProvider_CorrectlyOverrides(string profileName, string prov { Dictionary enabledBy = new Dictionary(); - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); foreach (var provider in parsedProviders) { @@ -35,10 +34,9 @@ public void DuplicateProvider_CorrectlyOverrides(string profileName, string prov Profile.MergeProfileAndProviders(selectedProfile, parsedProviders, enabledBy); var enabledProvider = parsedProviders.SingleOrDefault(p => p.Name == "Microsoft-Windows-DotNETRuntime"); - Assert.True(enabledProvider != default(Provider)); // Assert that our specified provider overrides the version in the profile - Assert.True(enabledProvider.Keywords == UInt64.MaxValue); + Assert.True(enabledProvider.Keywords == (long)(-1)); Assert.True(enabledProvider.EventLevel == EventLevel.Verbose); Assert.True(enabledBy[enabledProvider.Name] == "--providers"); } diff --git a/src/tests/dotnet-trace/ProviderParsing.cs b/src/tests/dotnet-trace/ProviderParsing.cs index 8d1930d4b9..a311a57466 100644 --- a/src/tests/dotnet-trace/ProviderParsing.cs +++ b/src/tests/dotnet-trace/ProviderParsing.cs @@ -2,9 +2,9 @@ // 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.NETCore.Client; using System; using Xunit; -using Microsoft.Diagnostics.Tools.RuntimeClient; using System.Collections.Generic; using System.Linq; @@ -17,26 +17,28 @@ public class ProviderParsingTests [InlineData("VeryCoolProvider:1:5:FilterAndPayloadSpecs=\"QuotedValue\"")] public void ValidProvider_CorrectlyParses(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); - Provider provider = parsedProviders.First(); + EventPipeProvider provider = parsedProviders.First(); Assert.True(provider.Name == "VeryCoolProvider"); Assert.True(provider.Keywords == 1); Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); - Assert.True(provider.FilterData == "FilterAndPayloadSpecs=\"QuotedValue\""); + Assert.True(provider.Arguments.Count == 1); + Assert.True(provider.Arguments["FilterAndPayloadSpecs"] == "\"QuotedValue\""); } [Theory] [InlineData("VeryCoolProvider:0x1:5:FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value\"")] public void ValidProviderFilter_CorrectlyParses(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); - Provider provider = parsedProviders.First(); + EventPipeProvider provider = parsedProviders.First(); Assert.True(provider.Name == "VeryCoolProvider"); Assert.True(provider.Keywords == 1); Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); - Assert.True(provider.FilterData == "FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value\""); + Assert.True(provider.Arguments.Count == 1); + Assert.True(provider.Arguments["FilterAndPayloadSpecs"] == "\"QuotedValue:-\r\nQuoted/Value\""); } [Theory] @@ -57,16 +59,16 @@ public void InvalidProvider_CorrectlyThrows(string providerToParse) [Theory] [InlineData("VeryCoolProvider:0xFFFFFFFFFFFFFFFF:5:FilterAndPayloadSpecs=\"QuotedValue\"")] - [InlineData("VeryCoolProvider::5:FilterAndPayloadSpecs=\"QuotedValue\"")] public void ValidProviderKeyword_CorrectlyParses(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); - Provider provider = parsedProviders.First(); + EventPipeProvider provider = parsedProviders.First(); Assert.True(provider.Name == "VeryCoolProvider"); - Assert.True(provider.Keywords == ulong.MaxValue); + Assert.True(provider.Keywords == (long)(-1)); Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); - Assert.True(provider.FilterData == "FilterAndPayloadSpecs=\"QuotedValue\""); + Assert.True(provider.Arguments.Count == 1); + Assert.True(provider.Arguments["FilterAndPayloadSpecs"] == "\"QuotedValue\""); } [Theory] @@ -74,13 +76,14 @@ public void ValidProviderKeyword_CorrectlyParses(string providerToParse) [InlineData("VeryCoolProvider:::FilterAndPayloadSpecs=\"QuotedValue\"")] public void ValidProviderEventLevel_CorrectlyParses(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); - Provider provider = parsedProviders.First(); + EventPipeProvider provider = parsedProviders.First(); Assert.True(provider.Name == "VeryCoolProvider"); - Assert.True(provider.Keywords == ulong.MaxValue); + Assert.True(provider.Keywords == (long)(-1)); Assert.True(provider.EventLevel == System.Diagnostics.Tracing.EventLevel.Verbose); - Assert.True(provider.FilterData == "FilterAndPayloadSpecs=\"QuotedValue\""); + Assert.True(provider.Arguments.Count == 1); + Assert.True(provider.Arguments["FilterAndPayloadSpecs"] == "\"QuotedValue\""); } [Theory] @@ -104,26 +107,29 @@ public void Invalidkeyword_CorrectlyThrows(string providerToParse) [InlineData("ProviderOne:1:1:FilterAndPayloadSpecs=\"QuotedValue\",ProviderTwo:0x2:2:key=value,ProviderThree:0x3:3:key=value")] public void MultipleValidProviders_CorrectlyParses(string providersToParse) { - List parsedProviders = Extensions.ToProviders(providersToParse); + List parsedProviders = Extensions.ToProviders(providersToParse); Assert.True(parsedProviders.Count == 3); - Provider providerOne = parsedProviders[0]; - Provider providerTwo = parsedProviders[1]; - Provider providerThree = parsedProviders[2]; + EventPipeProvider providerOne = parsedProviders[0]; + EventPipeProvider providerTwo = parsedProviders[1]; + EventPipeProvider providerThree = parsedProviders[2]; Assert.True(providerOne.Name == "ProviderOne"); Assert.True(providerOne.Keywords == 1); Assert.True(providerOne.EventLevel == System.Diagnostics.Tracing.EventLevel.Critical); - Assert.True(providerOne.FilterData == "FilterAndPayloadSpecs=\"QuotedValue\""); + Assert.True(providerOne.Arguments.Count == 1); + Assert.True(providerOne.Arguments["FilterAndPayloadSpecs"] == "\"QuotedValue\""); Assert.True(providerTwo.Name == "ProviderTwo"); Assert.True(providerTwo.Keywords == 2); Assert.True(providerTwo.EventLevel == System.Diagnostics.Tracing.EventLevel.Error); - Assert.True(providerTwo.FilterData == "key=value"); + Assert.True(providerTwo.Arguments.Count == 1); + Assert.True(providerTwo.Arguments["key"] == "value"); Assert.True(providerThree.Name == "ProviderThree"); Assert.True(providerThree.Keywords == 3); Assert.True(providerThree.EventLevel == System.Diagnostics.Tracing.EventLevel.Warning); - Assert.True(providerThree.FilterData == "key=value"); + Assert.True(providerThree.Arguments.Count == 1); + Assert.True(providerThree.Arguments["key"] == "value"); } [Theory] @@ -157,26 +163,29 @@ public void MultipleValidProvidersWithOneInvalidKeyword_CorrectlyThrows(string p [InlineData("ProviderOne:0x1:1:FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value:-A=B;C=D;\",ProviderTwo:2:2:FilterAndPayloadSpecs=\"QuotedValue\",ProviderThree:3:3:FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value:-A=B;C=D;\"")] public void MultipleProvidersWithComplexFilters_CorrectlyParse(string providersToParse) { - List parsedProviders = Extensions.ToProviders(providersToParse); + List parsedProviders = Extensions.ToProviders(providersToParse); Assert.True(parsedProviders.Count == 3); - Provider providerOne = parsedProviders[0]; - Provider providerTwo = parsedProviders[1]; - Provider providerThree = parsedProviders[2]; + EventPipeProvider providerOne = parsedProviders[0]; + EventPipeProvider providerTwo = parsedProviders[1]; + EventPipeProvider providerThree = parsedProviders[2]; Assert.True(providerOne.Name == "ProviderOne"); Assert.True(providerOne.Keywords == 1); Assert.True(providerOne.EventLevel == System.Diagnostics.Tracing.EventLevel.Critical); - Assert.True(providerOne.FilterData == "FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value:-A=B;C=D;\""); + Assert.True(providerOne.Arguments.Count == 1); + Assert.True(providerOne.Arguments["FilterAndPayloadSpecs"] == "\"QuotedValue:-\r\nQuoted/Value:-A=B;C=D;\""); Assert.True(providerTwo.Name == "ProviderTwo"); Assert.True(providerTwo.Keywords == 2); Assert.True(providerTwo.EventLevel == System.Diagnostics.Tracing.EventLevel.Error); - Assert.True(providerTwo.FilterData == "FilterAndPayloadSpecs=\"QuotedValue\""); + Assert.True(providerTwo.Arguments.Count == 1); + Assert.True(providerTwo.Arguments["FilterAndPayloadSpecs"]== "\"QuotedValue\""); Assert.True(providerThree.Name == "ProviderThree"); Assert.True(providerThree.Keywords == 3); Assert.True(providerThree.EventLevel == System.Diagnostics.Tracing.EventLevel.Warning); - Assert.True(providerThree.FilterData == "FilterAndPayloadSpecs=\"QuotedValue:-\r\nQuoted/Value:-A=B;C=D;\""); + Assert.True(providerThree.Arguments.Count == 1); + Assert.True(providerThree.Arguments["FilterAndPayloadSpecs"] == "\"QuotedValue:-\r\nQuoted/Value:-A=B;C=D;\""); } [Theory] @@ -184,7 +193,7 @@ public void MultipleProvidersWithComplexFilters_CorrectlyParse(string providersT [InlineData("ProviderOne:0x1:verbose")] public void TextLevelProviderSpecVerbose_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -196,7 +205,7 @@ public void TextLevelProviderSpecVerbose_CorrectlyParse(string providerToParse) [InlineData("ProviderOne:0x1:INFORMATIONAL")] public void TextLevelProviderSpecInformational_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -208,7 +217,7 @@ public void TextLevelProviderSpecInformational_CorrectlyParse(string providerToP [InlineData("ProviderOne:0x1:LogAlwayS")] public void TextLevelProviderSpecLogAlways_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -220,7 +229,7 @@ public void TextLevelProviderSpecLogAlways_CorrectlyParse(string providerToParse [InlineData("ProviderOne:0x1:ERRor")] public void TextLevelProviderSpecError_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -232,7 +241,7 @@ public void TextLevelProviderSpecError_CorrectlyParse(string providerToParse) [InlineData("ProviderOne:0x1:CRITICAL")] public void TextLevelProviderSpecCritical_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1); @@ -244,7 +253,7 @@ public void TextLevelProviderSpecCritical_CorrectlyParse(string providerToParse) [InlineData("ProviderOne:0x1:warning")] public void TextLevelProviderSpecWarning_CorrectlyParse(string providerToParse) { - List parsedProviders = Extensions.ToProviders(providerToParse); + List parsedProviders = Extensions.ToProviders(providerToParse); Assert.True(parsedProviders.Count == 1); Assert.True(parsedProviders[0].Name == "ProviderOne"); Assert.True(parsedProviders[0].Keywords == 1);