diff --git a/compileoptions.cmake b/compileoptions.cmake index 7bc7afbbc8..b31f265689 100644 --- a/compileoptions.cmake +++ b/compileoptions.cmake @@ -79,6 +79,11 @@ if(CLR_CMAKE_PLATFORM_UNIX_ARM) endif(ARM_SOFTFP) endif(CLR_CMAKE_PLATFORM_UNIX_ARM) +if(CLR_CMAKE_PLATFORM_FREEBSD) + add_compile_options(-Wno-macro-redefined) + add_compile_options(-Wno-pointer-to-int-cast) +endif(CLR_CMAKE_PLATFORM_FREEBSD) + if (WIN32) # Compile options for targeting windows diff --git a/documentation/design-docs/ipc-protocol.md b/documentation/design-docs/ipc-protocol.md index d5238a0071..42c28f7b30 100644 --- a/documentation/design-docs/ipc-protocol.md +++ b/documentation/design-docs/ipc-protocol.md @@ -376,8 +376,10 @@ See: [Profiler Commands](#Profiler-Commands) ```c++ enum class ProcessCommandId : uint8_t { - ProcessInfo = 0x00, - ResumeRuntime = 0x01, + ProcessInfo = 0x00, + ResumeRuntime = 0x01, + ProcessEnvironment = 0x02, + ProcessInfo2 = 0x04, // future } ``` @@ -787,6 +789,62 @@ struct Payload } ``` +> Available since .NET 6.0 + +### `ProcessInfo2` + +Command Code: `0x0404` + +The `ProcessInfo2` command queries the runtime for some basic information about the process. The returned payload has the same information as that of the `ProcessInfo` command in addition to the managed entrypoint assembly name and CLR product version. + +In the event of an [error](#Errors), the runtime will attempt to send an error message and subsequently close the connection. + +#### Inputs: + +Header: `{ Magic; Size; 0x0402; 0x0000 }` + +There is no payload. + +#### Returns (as an IPC Message Payload): + +Header: `{ Magic; size; 0xFF00; 0x0000; }` + +Payload: +* `int64 processId`: the process id in the process's PID-space +* `GUID runtimeCookie`: a 128-bit GUID that should be unique across PID-spaces +* `string commandLine`: the command line that invoked the process + * Windows: will be the same as the output of `GetCommandLineW` + * Non-Windows: will be the fully qualified path of the executable in `argv[0]` followed by all arguments as the appear in `argv` separated by spaces, i.e., `/full/path/to/argv[0] argv[1] argv[2] ...` +* `string OS`: the operating system that the process is running on + * macOS => `"macOS"` + * Windows => `"Windows"` + * Linux => `"Linux"` + * other => `"Unknown"` +* `string arch`: the architecture of the process + * 32-bit => `"x86"` + * 64-bit => `"x64"` + * ARM32 => `"arm32"` + * ARM64 => `"arm64"` + * Other => `"Unknown"` +* `string managedEntrypointAssemblyName`: the assembly name from the assembly identity of the entrypoint assembly of the process. This is the same value that is returned from executing `System.Reflection.Assembly.GetEntryAssembly().GetName().Name` in the target process. +* `string clrProductVersion`: the product version of the CLR of the process; may contain prerelease label information e.g. `6.0.0-preview.6.#####` + +##### Details: + +Returns: +```c++ +struct Payload +{ + uint64_t ProcessId; + LPCWSTR CommandLine; + LPCWSTR OS; + LPCWSTR Arch; + GUID RuntimeCookie; + LPCWSTR ManagedEntrypointAssemblyName; + LPCWSTR ClrProductVersion; +} +``` + ## Errors In the event an error occurs in the handling of an Ipc Message, the Diagnostic Server will attempt to send an Ipc Message encoding the error and subsequently close the connection. The connection will be closed **regardless** of the success of sending the error message. The Client is expected to be resilient in the event of a connection being abruptly closed. diff --git a/documentation/diagnostics-client-library-instructions.md b/documentation/diagnostics-client-library-instructions.md index 09fcfdd711..1ebddb12b4 100644 --- a/documentation/diagnostics-client-library-instructions.md +++ b/documentation/diagnostics-client-library-instructions.md @@ -224,14 +224,36 @@ public static void PrintEventsLive(int processId) This sample shows how to attach an ICorProfiler to a process (profiler attach). ```cs -public static int AttachProfiler(int processId, Guid profilerGuid, string profilerPath) +public static void AttachProfiler(int processId, Guid profilerGuid, string profilerPath) { var client = new DiagnosticsClient(processId); - return client.AttachProfiler(TimeSpan.FromSeconds(10), profilerGuid, profilerPath); + client.AttachProfiler(TimeSpan.FromSeconds(10), profilerGuid, profilerPath); } ``` +#### 8. Set an ICorProfiler to be used as the startup profiler +This sample shows how to request that the runtime use an ICorProfiler as the startup profiler (not as an attaching profiler). It is only valid to issue this command while the runtime is paused in "reverse server" mode. + +```cs +public static void SetStartupProfilerProfiler(Guid profilerGuid, string profilerPath) +{ + var client = new DiagnosticsClient(processId); + client.SetStartupProfiler(profilerGuid, profilerPath); +} +``` + +#### 9. Resume the runtime when it is paused in reverse server mode + +This sample shows how a client can instruct the runtime to resume loading after it has been paused in "reverse server" mode. + +```cs +public static void ResumeRuntime(Guid profilerGuid, string profilerPath) +{ + var client = new DiagnosticsClient(processId); + client.ResumeRuntime(); +} +``` ## API Description diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 90700bfcb7..d5c2b9bad6 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -8,13 +8,13 @@ https://github.com/dotnet/symstore 3ed87724fe4e98c7ecc77617720591783ee2e676 - + https://github.com/microsoft/clrmd - 52b244f9b62e7a4e398f0cd9cb99d3c9a76f3130 + 957981f36eeccb6e9d266407df6522ca5cfbd899 - + https://github.com/microsoft/clrmd - 52b244f9b62e7a4e398f0cd9cb99d3c9a76f3130 + 957981f36eeccb6e9d266407df6522ca5cfbd899 https://github.com/dotnet/installer @@ -32,21 +32,21 @@ 7f13798e5f567b72ffe63205bf49839245f0f8c1 - + https://github.com/dotnet/aspnetcore - e7b5aa6f713e9f040ba0730b915ae407d35971c1 + 837b17847c427be12d69623cf32223c10a4ddba5 - + https://github.com/dotnet/aspnetcore - e7b5aa6f713e9f040ba0730b915ae407d35971c1 + 837b17847c427be12d69623cf32223c10a4ddba5 - + https://github.com/dotnet/runtime - f584c7401a24781b4c8e8e2a8097b85462ee4941 + 98b7ed1a3b0543a31b5a0f9069cf44cb70c9230c - + https://github.com/dotnet/runtime - f584c7401a24781b4c8e8e2a8097b85462ee4941 + 98b7ed1a3b0543a31b5a0f9069cf44cb70c9230c diff --git a/eng/Versions.props b/eng/Versions.props index cba4dfbe20..a3b464b390 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -17,11 +17,11 @@ 5.0.6 $(MicrosoftNETCoreApp50Version) - 6.0.0-preview.6.21276.1 - 6.0.0-preview.6.21276.1 + 6.0.0-preview.7.21361.10 + 6.0.0-preview.7.21361.10 - 6.0.0-preview.6.21275.8 - 6.0.0-preview.6.21275.8 + 6.0.0-preview.7.21363.17 + 6.0.0-preview.7.21363.17 6.0.100-preview.1.21103.13 @@ -36,8 +36,8 @@ 4.3.0 1.1.0 - 2.0.226801 - 2.0.226801 + 2.0.230301 + 2.0.230301 16.9.0-beta1.21055.5 2.0.64 2.1.1 diff --git a/eng/build.sh b/eng/build.sh index d6df50ab3d..28bb8c8f08 100755 --- a/eng/build.sh +++ b/eng/build.sh @@ -100,7 +100,7 @@ case $CPUName in __HostArch=x86 ;; - x86_64) + x86_64|amd64) __BuildArch=x64 __HostArch=x64 ;; @@ -259,7 +259,7 @@ while :; do -clang*) __Compiler=clang # clangx.y or clang-x.y - version="$(echo "$lowerI" | tr -d '[:alpha:]-=')" + version="$(echo "$1" | tr -d '[:alpha:]-=')" parts=(${version//./ }) __ClangMajorVersion="${parts[0]}" __ClangMinorVersion="${parts[1]}" @@ -433,15 +433,6 @@ if [ "$__HostOS" == "OSX" ]; then export MACOSX_DEPLOYMENT_TARGET=10.12 - # If Xcode 9.2 exists (like on the CI/build machines), use that. Xcode 9.3 or - # greater (swift 4.1 lldb) doesn't work that well (seg faults on exit). - if [ -f "/Applications/Xcode_9.2.app/Contents/Developer/usr/bin/lldb" ]; then - if [ -f "/Applications/Xcode_9.2.app/Contents/SharedFrameworks/LLDB.framework/LLDB" ]; then - export LLDB_PATH=/Applications/Xcode_9.2.app/Contents/Developer/usr/bin/lldb - export LLDB_LIB=/Applications/Xcode_9.2.app/Contents/SharedFrameworks/LLDB.framework/LLDB - fi - fi - if [ ! -f $LLDB_LIB ]; then echo "Cannot find the lldb library. Try installing Xcode." exit 1 diff --git a/eng/common/native/find-native-compiler.sh b/eng/common/native/find-native-compiler.sh index aed19d07d5..289af7eed1 100644 --- a/eng/common/native/find-native-compiler.sh +++ b/eng/common/native/find-native-compiler.sh @@ -45,6 +45,10 @@ check_version_exists() { desired_version="$1$2" elif command -v "$compiler-$1$2" > /dev/null; then desired_version="-$1$2" + elif command -v "$compiler$1" > /dev/null; then + desired_version="$1" + elif command -v "$compiler-$1" > /dev/null; then + desired_version="-$1" fi echo "$desired_version" @@ -55,7 +59,7 @@ if [ -z "$CLR_CC" ]; then # Set default versions if [ -z "$majorVersion" ]; then # note: gcc (all versions) and clang versions higher than 6 do not have minor version in file name, if it is zero. - if [ "$compiler" = "clang" ]; then versions=( 9 8 7 6.0 5.0 4.0 3.9 3.8 3.7 3.6 3.5 ) + if [ "$compiler" = "clang" ]; then versions=( 12 11 10 9 8 7 6.0 5.0 4.0 3.9 3.8 3.7 3.6 3.5 ) elif [ "$compiler" = "gcc" ]; then versions=( 9 8 7 6 5 4.9 ); fi for version in "${versions[@]}"; do diff --git a/eng/gen-buildsys-clang.sh b/eng/gen-buildsys-clang.sh index 2f8dd790e1..a805d1e808 100755 --- a/eng/gen-buildsys-clang.sh +++ b/eng/gen-buildsys-clang.sh @@ -29,6 +29,12 @@ elif command -v "clang$2$3" > /dev/null elif command -v "clang-$2$3" > /dev/null then desired_llvm_version="-$2$3" +elif command -v "clang-$2" > /dev/null + then + desired_llvm_version="-$2" +elif command -v "clang$2" > /dev/null + then + desired_llvm_version="$2" elif command -v clang > /dev/null then desired_llvm_version= diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs index 88d418c0e3..09f68ca5df 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs @@ -34,9 +34,9 @@ public enum Flags : byte private readonly IDisposable _onChangeEvent; private Flags _flags; - private PdbInfo _pdbInfo; + private PdbFileInfo _pdbFileInfo; private ImmutableArray _buildId; - private VersionInfo? _version; + private VersionData _versionData; private PEImage _peImage; public readonly ServiceProvider ServiceProvider; @@ -118,12 +118,12 @@ public bool? IsFileLayout } } - public PdbInfo PdbInfo + public PdbFileInfo PdbFileInfo { get { GetPEInfo(); - return _pdbInfo; + return _pdbFileInfo; } } @@ -147,10 +147,10 @@ public ImmutableArray BuildId } } - public virtual VersionInfo? Version + public virtual VersionData VersionData { - get { return _version; } - set { _version = value; } + get { return _versionData; } + set { _versionData = value; } } public abstract string VersionString { get; } @@ -162,7 +162,7 @@ protected void GetVersionFromVersionString() GetPEInfo(); // If we can't get the version from the PE, search for version string embedded in the module data - if (!_version.HasValue && !IsPEImage) + if (_versionData is null && !IsPEImage) { string versionString = VersionString; if (versionString != null) @@ -178,7 +178,7 @@ protected void GetVersionFromVersionString() try { Version version = System.Version.Parse(versionToParse); - _version = new VersionInfo(version.Major, version.Minor, version.Build, version.Revision); + _versionData = new VersionData(version.Major, version.Minor, version.Build, version.Revision); } catch (ArgumentException ex) { @@ -192,7 +192,7 @@ protected void GetVersionFromVersionString() protected PEImage GetPEInfo() { if (InitializeValue(Flags.InitializePEInfo)) { - _peImage = ModuleService.GetPEInfo(ImageBase, ImageSize, ref _pdbInfo, ref _version, ref _flags); + _peImage = ModuleService.GetPEInfo(ImageBase, ImageSize, ref _pdbFileInfo, ref _versionData, ref _flags); } return _peImage; } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs index 8dc1c0be00..65772fc804 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.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.Runtime; using Microsoft.Diagnostics.Runtime.Utilities; using Microsoft.FileFormats; using Microsoft.FileFormats.ELF; @@ -203,11 +202,11 @@ private IModule[] GetSortedModules() /// /// module base address /// module size - /// the pdb record or null - /// the PE version or null + /// the pdb record or null + /// the PE version or null /// module flags /// PEImage instance or null - internal PEImage GetPEInfo(ulong address, ulong size, ref PdbInfo pdbInfo, ref VersionInfo? version, ref Module.Flags flags) + internal PEImage GetPEInfo(ulong address, ulong size, ref PdbFileInfo pdbFileInfo, ref VersionData versionData, ref Module.Flags flags) { PEImage peImage = null; @@ -215,13 +214,13 @@ internal PEImage GetPEInfo(ulong address, ulong size, ref PdbInfo pdbInfo, ref V if (Target.Host.HostType != HostType.Lldb) { // First try getting the PE info as load layout (native Windows DLLs and most managed PEs on Linux/MacOS). - peImage = GetPEInfo(isVirtual: true, address, size, ref pdbInfo, ref version, ref flags); + peImage = GetPEInfo(isVirtual: true, address, size, ref pdbFileInfo, ref versionData, ref flags); if (peImage == null) { if (Target.OperatingSystem != OSPlatform.Windows) { // Then try getting the PE info as file layout (some managed PEs on Linux/MacOS). - peImage = GetPEInfo(isVirtual: false, address, size, ref pdbInfo, ref version, ref flags); + peImage = GetPEInfo(isVirtual: false, address, size, ref pdbFileInfo, ref versionData, ref flags); } } } @@ -234,11 +233,11 @@ internal PEImage GetPEInfo(ulong address, ulong size, ref PdbInfo pdbInfo, ref V /// the memory layout of the module /// module base address /// module size - /// the pdb record or null - /// the PE version or null + /// the pdb record or null + /// the PE version or null /// module flags /// PEImage instance or null - private PEImage GetPEInfo(bool isVirtual, ulong address, ulong size, ref PdbInfo pdbInfo, ref VersionInfo? version, ref Module.Flags flags) + private PEImage GetPEInfo(bool isVirtual, ulong address, ulong size, ref PdbFileInfo pdbFileInfo, ref VersionData versionData, ref Module.Flags flags) { Stream stream = MemoryService.CreateMemoryStream(address, size); try @@ -249,13 +248,13 @@ private PEImage GetPEInfo(bool isVirtual, ulong address, ulong size, ref PdbInfo { flags |= Module.Flags.IsPEImage; flags |= peImage.IsManaged ? Module.Flags.IsManaged : Module.Flags.None; - pdbInfo = peImage.DefaultPdb; - if (!version.HasValue) + pdbFileInfo = peImage.DefaultPdb.ToPdbFileInfo(); + if (versionData is null) { FileVersionInfo fileVersionInfo = peImage.GetFileVersionInfo(); if (fileVersionInfo != null) { - version = fileVersionInfo.VersionInfo; + versionData = fileVersionInfo.VersionInfo.ToVersionData(); } } flags &= ~(Module.Flags.IsLoadedLayout | Module.Flags.IsFileLayout); diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleServiceFromDataReader.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleServiceFromDataReader.cs index de0d8b27f5..1ee7ae131d 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleServiceFromDataReader.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleServiceFromDataReader.cs @@ -22,7 +22,7 @@ class ModuleFromDataReader : Module, IExportSymbols // This is what clrmd returns for non-PE modules that don't have a timestamp private const uint InvalidTimeStamp = 0; - private static readonly VersionInfo EmptyVersionInfo = new VersionInfo(0, 0, 0, 0); + private static readonly Microsoft.Diagnostics.Runtime.VersionInfo EmptyVersionInfo = new (0, 0, 0, 0); private readonly ModuleServiceFromDataReader _moduleService; private readonly IExportReader _exportReader; private readonly ModuleInfo _moduleInfo; @@ -57,7 +57,7 @@ public ModuleFromDataReader(ModuleServiceFromDataReader moduleService, IExportRe public override uint? IndexTimeStamp => _moduleInfo.IndexTimeStamp == InvalidTimeStamp ? null : (uint)_moduleInfo.IndexTimeStamp; - public override VersionInfo? Version + public override VersionData VersionData { get { @@ -65,7 +65,7 @@ public override VersionInfo? Version { if (_moduleInfo.Version != EmptyVersionInfo) { - base.Version = _moduleInfo.Version; + base.VersionData = _moduleInfo.Version.ToVersionData(); } else { @@ -75,7 +75,7 @@ public override VersionInfo? Version } } } - return base.Version; + return base.VersionData; } } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeService.cs index c54516c913..b5283be687 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeService.cs @@ -134,14 +134,14 @@ ImmutableArray IDataReader.GetBuildId(ulong baseAddress) return ImmutableArray.Empty; } - bool IDataReader.GetVersionInfo(ulong baseAddress, out VersionInfo version) + bool IDataReader.GetVersionInfo(ulong baseAddress, out Microsoft.Diagnostics.Runtime.VersionInfo version) { try { - VersionInfo? v = ModuleService.GetModuleFromBaseAddress(baseAddress).Version; - if (v.HasValue) + VersionData versionData = ModuleService.GetModuleFromBaseAddress(baseAddress).VersionData; + if (versionData is not null) { - version = v.Value; + version = versionData.ToVersionInfo(); return true; } } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs index c1cc2618cf..9c0035cdbd 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs @@ -4,7 +4,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { - public class Utilities + public static class Utilities { /// /// Combines two hash codes into a single hash code, in an order-dependent manner. @@ -23,5 +23,29 @@ public static int CombineHashCodes(int hashCode0, int hashCode1) return hashCode0 ^ (hashCode1 + (int) 0x9e3779b9 + (hashCode0 << 6) + (hashCode0 >> 2)); } } + + /// + /// Convert from CLRMD VersionInfo to DebugServices VersionData + /// + public static VersionData ToVersionData(this Microsoft.Diagnostics.Runtime.VersionInfo versionInfo) + { + return new VersionData(versionInfo.Major, versionInfo.Minor, versionInfo.Revision, versionInfo.Patch); + } + + /// + /// Convert from DebugServices VersionData to CLRMD VersionInfo + /// + public static Microsoft.Diagnostics.Runtime.VersionInfo ToVersionInfo(this VersionData versionData) + { + return new Microsoft.Diagnostics.Runtime.VersionInfo(versionData.Major, versionData.Minor, versionData.Revision, versionData.Patch); + } + + /// + /// Convert from CLRMD PdbInfo to DebugServices PdbFileInfo + /// + public static PdbFileInfo ToPdbFileInfo(this Microsoft.Diagnostics.Runtime.PdbInfo pdbInfo) + { + return new PdbFileInfo(pdbInfo.Path, pdbInfo.Guid, pdbInfo.Revision); + } } } diff --git a/src/Microsoft.Diagnostics.DebugServices/IModule.cs b/src/Microsoft.Diagnostics.DebugServices/IModule.cs index 3f346e4d2b..da9ce58c9a 100644 --- a/src/Microsoft.Diagnostics.DebugServices/IModule.cs +++ b/src/Microsoft.Diagnostics.DebugServices/IModule.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.Runtime; using System; using System.Collections.Immutable; @@ -39,7 +38,7 @@ public interface IModule ulong ImageBase { get; } /// - /// Returns the image size of module in memory + /// Returns the image size of module in memory. /// ulong ImageSize { get; } @@ -59,29 +58,29 @@ public interface IModule ImmutableArray BuildId { get; } /// - /// Returns true if Windows PE format image (native or IL) + /// Returns true if Windows PE format image (native or IL). /// bool IsPEImage { get; } /// - /// Returns true if managed or IL assembly + /// Returns true if managed or IL assembly. /// bool IsManaged { get; } /// - /// Returns true if the PE module is layout is file. False, layout is loaded image. Null, not a PE image. + /// Returns true if the PE module is layout is file. False, layout is loaded image. If null, not a PE image. /// bool? IsFileLayout { get; } /// - /// PDB information for Windows PE modules (managed or native + /// PDB information for Windows PE modules (managed or native). /// - PdbInfo PdbInfo { get; } + PdbFileInfo PdbFileInfo { get; } /// /// Version information for Window PE modules (managed or native). /// - VersionInfo? Version { get; } + VersionData VersionData { get; } /// /// This is the file version string containing the build version and commit id. diff --git a/src/Microsoft.Diagnostics.DebugServices/Microsoft.Diagnostics.DebugServices.csproj b/src/Microsoft.Diagnostics.DebugServices/Microsoft.Diagnostics.DebugServices.csproj index 3c0a83d0bb..08b9efdb04 100644 --- a/src/Microsoft.Diagnostics.DebugServices/Microsoft.Diagnostics.DebugServices.csproj +++ b/src/Microsoft.Diagnostics.DebugServices/Microsoft.Diagnostics.DebugServices.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Microsoft.Diagnostics.DebugServices/PdbFileInfo.cs b/src/Microsoft.Diagnostics.DebugServices/PdbFileInfo.cs new file mode 100644 index 0000000000..f538e05c7d --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/PdbFileInfo.cs @@ -0,0 +1,41 @@ +// 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.DebugServices +{ + /// + /// Information about a specific PDB instance obtained from a PE image. + /// + public sealed class PdbFileInfo + { + /// + /// Gets the Guid of the PDB. + /// + public Guid Guid { get; } + + /// + /// Gets the PDB revision. + /// + public int Revision { get; } + + /// + /// Gets the path to the PDB. + /// + public string Path { get; } + + /// + /// Creates an instance of the PdbInfo with the corresponding properties initialized. + /// + public PdbFileInfo(string path, Guid guid, int revision) + { + Path = path; + Guid = guid; + Revision = revision; + } + + public override string ToString() => $"{Guid} {Revision} {Path}"; + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/RegisterInfo.cs b/src/Microsoft.Diagnostics.DebugServices/RegisterInfo.cs index a97b92ccba..b815a613b7 100644 --- a/src/Microsoft.Diagnostics.DebugServices/RegisterInfo.cs +++ b/src/Microsoft.Diagnostics.DebugServices/RegisterInfo.cs @@ -7,7 +7,7 @@ namespace Microsoft.Diagnostics.DebugServices /// /// Details about a register /// - public struct RegisterInfo + public readonly struct RegisterInfo { public readonly int RegisterIndex; public readonly int RegisterOffset; diff --git a/src/Microsoft.Diagnostics.DebugServices/VersionData.cs b/src/Microsoft.Diagnostics.DebugServices/VersionData.cs new file mode 100644 index 0000000000..5a2625d264 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/VersionData.cs @@ -0,0 +1,102 @@ +// 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.DebugServices +{ + /// + /// Represents the version of a module + /// + public sealed class VersionData : IEquatable, IComparable + { + /// + /// In a version 'A.B.C.D', this field represents 'A'. + /// + public int Major { get; } + + /// + /// In a version 'A.B.C.D', this field represents 'B'. + /// + public int Minor { get; } + + /// + /// In a version 'A.B.C.D', this field represents 'C'. + /// + public int Revision { get; } + + /// + /// In a version 'A.B.C.D', this field represents 'D'. + /// + public int Patch { get; } + + public VersionData(int major, int minor, int revision, int patch) + { + if (major < 0) + throw new ArgumentOutOfRangeException(nameof(major)); + + if (minor < 0) + throw new ArgumentOutOfRangeException(nameof(minor)); + + if (revision < 0) + throw new ArgumentOutOfRangeException(nameof(revision)); + + if (patch < 0) + throw new ArgumentOutOfRangeException(nameof(patch)); + + Major = major; + Minor = minor; + Revision = revision; + Patch = patch; + } + + /// + public bool Equals(VersionData other) => Major == other.Major && Minor == other.Minor && Revision == other.Revision && Patch == other.Patch; + + /// + public override bool Equals(object obj) => obj is VersionData other && Equals(other); + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = Major; + hashCode = (hashCode * 397) ^ Minor; + hashCode = (hashCode * 397) ^ Revision; + hashCode = (hashCode * 397) ^ Patch; + return hashCode; + } + } + + /// + public int CompareTo(VersionData other) + { + if (Major != other.Major) + return Major.CompareTo(other.Major); + + if (Minor != other.Minor) + return Minor.CompareTo(other.Minor); + + if (Revision != other.Revision) + return Revision.CompareTo(other.Revision); + + return Patch.CompareTo(other.Patch); + } + + public override string ToString() => $"{Major}.{Minor}.{Revision}.{Patch}"; + + public static bool operator ==(VersionData left, VersionData right) => left.Equals(right); + + public static bool operator !=(VersionData left, VersionData right) => !(left == right); + + public static bool operator <(VersionData left, VersionData right) => left.CompareTo(right) < 0; + + public static bool operator <=(VersionData left, VersionData right) => left.CompareTo(right) <= 0; + + public static bool operator >(VersionData left, VersionData right) => right < left; + + public static bool operator >=(VersionData left, VersionData right) => right <= left; + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs index 20c7dc879c..ed5547f60f 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs @@ -4,8 +4,6 @@ using Microsoft.Diagnostics.DebugServices; using Microsoft.Diagnostics.Runtime; -using System; -using System.Linq; namespace Microsoft.Diagnostics.ExtensionCommands { @@ -40,10 +38,10 @@ public override void Invoke() WriteLine(" MetadataAddress: {0:X16}", module.MetadataAddress); WriteLine(" MetadataSize: {0:X16}", module.MetadataLength); WriteLine(" PdbInfo: {0}", module.Pdb?.ToString() ?? ""); - VersionInfo? version = null; + VersionData version = null; try { - version = ModuleService.GetModuleFromBaseAddress(module.ImageBase).Version; + version = ModuleService.GetModuleFromBaseAddress(module.ImageBase).VersionData; } catch (DiagnosticsException) { diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs index 3b14736730..87c39f69d8 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs @@ -32,12 +32,12 @@ public override void Invoke() WriteLine(" IsFileLayout: {0}", module.IsFileLayout?.ToString() ?? ""); WriteLine(" IndexFileSize: {0}", module.IndexFileSize?.ToString("X8") ?? ""); WriteLine(" IndexTimeStamp: {0}", module.IndexTimeStamp?.ToString("X8") ?? ""); - WriteLine(" Version: {0}", module.Version?.ToString() ?? ""); + WriteLine(" Version: {0}", module.VersionData?.ToString() ?? ""); string versionString = module.VersionString; if (!string.IsNullOrEmpty(versionString)) { WriteLine(" {0}", versionString); } - WriteLine(" PdbInfo: {0}", module.PdbInfo?.ToString() ?? ""); + WriteLine(" PdbInfo: {0}", module.PdbFileInfo?.ToString() ?? ""); WriteLine(" BuildId: {0}", !module.BuildId.IsDefaultOrEmpty ? string.Concat(module.BuildId.Select((b) => b.ToString("x2"))) : ""); } else diff --git a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj index 9c3591b506..ec8330ee18 100644 --- a/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj +++ b/src/Microsoft.Diagnostics.Monitoring.EventPipe/Microsoft.Diagnostics.Monitoring.EventPipe.csproj @@ -42,5 +42,6 @@ + diff --git a/src/Microsoft.Diagnostics.Monitoring/ClientEndpointInfoSource.cs b/src/Microsoft.Diagnostics.Monitoring/ClientEndpointInfoSource.cs deleted file mode 100644 index 7cac048cb3..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/ClientEndpointInfoSource.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 System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Diagnostics.NETCore.Client; - -namespace Microsoft.Diagnostics.Monitoring -{ - internal sealed class ClientEndpointInfoSource : IEndpointInfoSourceInternal - { - public async Task> GetEndpointInfoAsync(CancellationToken token) - { - var endpointInfoTasks = new List>(); - // Run the EndpointInfo creation parallel. The call to FromProcessId sends - // a GetProcessInfo command to the runtime instance to get additional information. - foreach (int pid in DiagnosticsClient.GetPublishedProcesses()) - { - endpointInfoTasks.Add(Task.Run(() => - { - try - { - return EndpointInfo.FromProcessId(pid); - } - //Catch when the application is running a more privilaged socket than dotnet-monitor. For example, running a web app as administrator - //while running dotnet-monitor without elevation. - catch (UnauthorizedAccessException) - { - return null; - } - //Most errors from IpcTransport, such as a stale socket. - catch (ServerNotAvailableException) - { - return null; - } - }, token)); - } - - await Task.WhenAll(endpointInfoTasks); - - return endpointInfoTasks.Where(t => t.Result != null).Select(t => t.Result); - } - } -} diff --git a/src/Microsoft.Diagnostics.Monitoring/CommandLineHelper.cs b/src/Microsoft.Diagnostics.Monitoring/CommandLineHelper.cs deleted file mode 100644 index 653467e841..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/CommandLineHelper.cs +++ /dev/null @@ -1,69 +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 System.Text; - -namespace Microsoft.Diagnostics.Monitoring -{ - internal class CommandLineHelper - { - public static string ExtractExecutablePath(string commandLine, bool isWindows) - { - if (string.IsNullOrEmpty(commandLine)) - { - return commandLine; - } - - int commandLineLength = commandLine.Length; - bool isQuoted = false; - bool isEscaped = false; - int i = 0; - char c = commandLine[0]; - - // Search for the first whitespace character that is not quoted. - // Store character literals as it iterates the command line. Escaped - // characters within double quotes are unescaped for non-Windows systems. - // Algorithm based on INIT_FormatCommandLine behavior from - // https://github.com/dotnet/runtime/blob/main/src/coreclr/pal/src/init/pal.cpp - StringBuilder builder = new StringBuilder(commandLineLength); - do - { - if (isEscaped) - { - builder.Append(c); - isEscaped = false; - } - else if (c == '"') - { - isQuoted = !isQuoted; - } - else if (c == '\\' && !isWindows) - { - if (isQuoted) - { - isEscaped = true; - } - else - { - builder.Append(c); - } - } - else - { - builder.Append(c); - } - - if (commandLineLength == ++i) - { - break; - } - - c = commandLine[i]; - } - while (isQuoted || !char.IsWhiteSpace(c)); - - return builder.ToString(); - } - } -} diff --git a/src/Microsoft.Diagnostics.Monitoring/EndpointInfo.cs b/src/Microsoft.Diagnostics.Monitoring/EndpointInfo.cs deleted file mode 100644 index dbac65f2ed..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/EndpointInfo.cs +++ /dev/null @@ -1,99 +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 System; -using System.Diagnostics; -using Microsoft.Diagnostics.NETCore.Client; - -namespace Microsoft.Diagnostics.Monitoring -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal class EndpointInfo : IEndpointInfo - { - public static EndpointInfo FromProcessId(int processId) - { - var client = new DiagnosticsClient(processId); - - ProcessInfo processInfo = null; - try - { - // Primary motivation is to get the runtime instance cookie in order to - // keep parity with the FromIpcEndpointInfo implementation; store the - // remainder of the information since it already has access to it. - processInfo = client.GetProcessInfo(); - - Debug.Assert(processId == unchecked((int)processInfo.ProcessId)); - } - catch (ServerErrorException) - { - // The runtime likely doesn't understand the GetProcessInfo command. - } - catch (TimeoutException) - { - // Runtime didn't respond within client timeout. - } - - // CONSIDER: Generate a runtime instance identifier based on the pipe name - // for .NET Core 3.1 e.g. pid + disambiguator in GUID form. - return new EndpointInfo() - { - Endpoint = new PidIpcEndpoint(processId), - ProcessId = processId, - RuntimeInstanceCookie = processInfo?.RuntimeInstanceCookie ?? Guid.Empty, - CommandLine = processInfo?.CommandLine, - OperatingSystem = processInfo?.OperatingSystem, - ProcessArchitecture = processInfo?.ProcessArchitecture - }; - } - - public static EndpointInfo FromIpcEndpointInfo(IpcEndpointInfo info) - { - var client = new DiagnosticsClient(info.Endpoint); - - ProcessInfo processInfo = null; - try - { - // Primary motivation is to keep parity with the FromProcessId implementation, - // which provides the additional process information because it already has - // access to it. - processInfo = client.GetProcessInfo(); - - Debug.Assert(info.ProcessId == unchecked((int)processInfo.ProcessId)); - Debug.Assert(info.RuntimeInstanceCookie == processInfo.RuntimeInstanceCookie); - } - catch (ServerErrorException) - { - // The runtime likely doesn't understand the GetProcessInfo command. - } - catch (TimeoutException) - { - // Runtime didn't respond within client timeout. - } - - return new EndpointInfo() - { - Endpoint = info.Endpoint, - ProcessId = info.ProcessId, - RuntimeInstanceCookie = info.RuntimeInstanceCookie, - CommandLine = processInfo?.CommandLine, - OperatingSystem = processInfo?.OperatingSystem, - ProcessArchitecture = processInfo?.ProcessArchitecture - }; - } - - public IpcEndpoint Endpoint { get; private set; } - - public int ProcessId { get; private set; } - - public Guid RuntimeInstanceCookie { get; private set; } - - public string CommandLine { get; private set; } - - public string OperatingSystem { get; private set; } - - public string ProcessArchitecture { get; private set; } - - internal string DebuggerDisplay => FormattableString.Invariant($"PID={ProcessId}, Cookie={RuntimeInstanceCookie}"); - } -} diff --git a/src/Microsoft.Diagnostics.Monitoring/IEndpointInfoSource.cs b/src/Microsoft.Diagnostics.Monitoring/IEndpointInfoSource.cs deleted file mode 100644 index f3b90b50e8..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/IEndpointInfoSource.cs +++ /dev/null @@ -1,36 +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 System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Diagnostics.NETCore.Client; - -namespace Microsoft.Diagnostics.Monitoring -{ - internal interface IEndpointInfo - { - IpcEndpoint Endpoint { get; } - - int ProcessId { get; } - - Guid RuntimeInstanceCookie { get; } - - string CommandLine { get; } - - string OperatingSystem { get; } - - string ProcessArchitecture { get; } - } - - public interface IEndpointInfoSource - { - } - - internal interface IEndpointInfoSourceInternal : IEndpointInfoSource - { - Task> GetEndpointInfoAsync(CancellationToken token); - } -} diff --git a/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj b/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj index f65df4500a..3dcce22cc6 100644 --- a/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj +++ b/src/Microsoft.Diagnostics.Monitoring/Microsoft.Diagnostics.Monitoring.csproj @@ -32,5 +32,6 @@ + diff --git a/src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs b/src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs deleted file mode 100644 index 270b1703ad..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/RuntimeInfo.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace Microsoft.Diagnostics.Monitoring -{ - public static class RuntimeInfo - { - public static bool IsDiagnosticsEnabled - { - get - { - string enableDiagnostics = Environment.GetEnvironmentVariable("COMPlus_EnableDiagnostics"); - return string.IsNullOrEmpty(enableDiagnostics) || !"0".Equals(enableDiagnostics, StringComparison.Ordinal); - } - } - - public static bool IsInDockerContainer - { - get - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - // Check if one of the control groups of this process is owned by docker - if (File.ReadAllText("/proc/self/cgroup").Contains("/docker/")) - { - return true; - } - - // Most of the control groups are owned by "kubepods" when running in kubernetes; - // Check for docker environment file - return File.Exists("/.dockerenv"); - } - - // TODO: Add detection for other platforms - return false; - } - } - - public static bool IsInKubernetes => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST")); - } -} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs b/src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs deleted file mode 100644 index 615f4c902c..0000000000 --- a/src/Microsoft.Diagnostics.Monitoring/ServerEndpointInfoSource.cs +++ /dev/null @@ -1,271 +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 System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Diagnostics.NETCore.Client; - -namespace Microsoft.Diagnostics.Monitoring -{ - /// - /// Aggregates diagnostic endpoints that are established at a transport path via a reversed server. - /// - internal class ServerEndpointInfoSource : IEndpointInfoSourceInternal, IAsyncDisposable - { - // The amount of time to wait when checking if the a endpoint info should be - // pruned from the list of endpoint infos. If the runtime doesn't have a viable connection within - // this time, it will be pruned from the list. - private static readonly TimeSpan PruneWaitForConnectionTimeout = TimeSpan.FromMilliseconds(250); - - private readonly CancellationTokenSource _cancellation = new CancellationTokenSource(); - private readonly IList _endpointInfos = new List(); - private readonly SemaphoreSlim _endpointInfosSemaphore = new SemaphoreSlim(1); - private readonly string _transportPath; - - private Task _listenTask; - private bool _disposed = false; - private ReversedDiagnosticsServer _server; - - /// - /// Constructs a that aggreates diagnostic endpoints - /// from a reversed diagnostics server at path specified by . - /// - /// - /// The path of the server endpoint. - /// On Windows, this can be a full pipe path or the name without the "\\.\pipe\" prefix. - /// On all other systems, this must be the full file path of the socket. - /// - public ServerEndpointInfoSource(string transportPath) - { - _transportPath = transportPath; - } - - public async ValueTask DisposeAsync() - { - if (!_disposed) - { - _cancellation.Cancel(); - - if (null != _listenTask) - { - try - { - await _listenTask.ConfigureAwait(false); - } - catch (Exception ex) - { - Debug.Fail(ex.Message); - } - } - - if (null != _server) - { - await _server.DisposeAsync().ConfigureAwait(false); - } - - _endpointInfosSemaphore.Dispose(); - - _cancellation.Dispose(); - - _disposed = true; - } - } - - /// - /// Starts listening to the reversed diagnostics server for new connections. - /// - public void Start() - { - Start(ReversedDiagnosticsServer.MaxAllowedConnections); - } - - /// - /// Starts listening to the reversed diagnostics server for new connections. - /// - /// The maximum number of connections the server will support. - public void Start(int maxConnections) - { - VerifyNotDisposed(); - - if (IsListening) - { - throw new InvalidOperationException(nameof(ServerEndpointInfoSource.Start) + " method can only be called once."); - } - - _server = new ReversedDiagnosticsServer(_transportPath); - - _listenTask = ListenAsync(maxConnections, _cancellation.Token); - } - - /// - /// Gets the list of served from the reversed diagnostics server. - /// - /// The token to monitor for cancellation requests. - /// A list of active instances. - public async Task> GetEndpointInfoAsync(CancellationToken token) - { - VerifyNotDisposed(); - - VerifyIsListening(); - - using CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, _cancellation.Token); - CancellationToken linkedToken = linkedSource.Token; - - // Prune connections that no longer have an active runtime instance before - // returning the list of connections. - await _endpointInfosSemaphore.WaitAsync(linkedToken).ConfigureAwait(false); - - try - { - // Check the transport for each endpoint info and remove it if the check fails. - IDictionary> checkMap = new Dictionary>(); - foreach (EndpointInfo info in _endpointInfos) - { - checkMap.Add(info, Task.Run(() => CheckNotViable(info, linkedToken), linkedToken)); - } - - // Wait for all checks to complete - await Task.WhenAll(checkMap.Values).ConfigureAwait(false); - - // Remove connections for failed checks - foreach (KeyValuePair> entry in checkMap) - { - if (entry.Value.Result) - { - _endpointInfos.Remove(entry.Key); - OnRemovedEndpointInfo(entry.Key); - _server?.RemoveConnection(entry.Key.RuntimeInstanceCookie); - } - } - - return _endpointInfos.ToList(); - } - finally - { - _endpointInfosSemaphore.Release(); - } - } - - /// - /// Returns true if the connection is not longer viable. - /// - private static async Task CheckNotViable(EndpointInfo info, CancellationToken token) - { - using var timeoutSource = new CancellationTokenSource(); - using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutSource.Token); - - try - { - timeoutSource.CancelAfter(PruneWaitForConnectionTimeout); - - await info.Endpoint.WaitForConnectionAsync(linkedSource.Token).ConfigureAwait(false); - } - catch - { - // Only report not viable if check was not cancelled. - if (!token.IsCancellationRequested) - { - return true; - } - } - - return false; - } - - /// - /// Accepts endpoint infos from the reversed diagnostics server. - /// - /// The token to monitor for cancellation requests. - private async Task ListenAsync(int maxConnections, CancellationToken token) - { - _server.Start(maxConnections); - // Continuously accept endpoint infos from the reversed diagnostics server so - // that - // is always awaited in order to to handle new runtime instance connections - // as well as existing runtime instance reconnections. - while (!token.IsCancellationRequested) - { - try - { - IpcEndpointInfo info = await _server.AcceptAsync(token).ConfigureAwait(false); - - _ = Task.Run(() => ResumeAndQueueEndpointInfo(info, token), token); - } - catch (OperationCanceledException) - { - } - } - } - - private async Task ResumeAndQueueEndpointInfo(IpcEndpointInfo info, CancellationToken token) - { - try - { - // Send ResumeRuntime message for runtime instances that connect to the server. This will allow - // those instances that are configured to pause on start to resume after the diagnostics - // connection has been made. Instances that are not configured to pause on startup will ignore - // the command and return success. - var client = new DiagnosticsClient(info.Endpoint); - try - { - client.ResumeRuntime(); - } - catch (ServerErrorException) - { - // The runtime likely doesn't understand the ResumeRuntime command. - } - - EndpointInfo endpointInfo = EndpointInfo.FromIpcEndpointInfo(info); - - await _endpointInfosSemaphore.WaitAsync(token).ConfigureAwait(false); - try - { - _endpointInfos.Add(endpointInfo); - - OnAddedEndpointInfo(endpointInfo); - } - finally - { - _endpointInfosSemaphore.Release(); - } - } - catch (Exception) - { - _server?.RemoveConnection(info.RuntimeInstanceCookie); - - throw; - } - } - - internal virtual void OnAddedEndpointInfo(EndpointInfo info) - { - } - - internal virtual void OnRemovedEndpointInfo(EndpointInfo info) - { - } - - private void VerifyNotDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(ServerEndpointInfoSource)); - } - } - - private void VerifyIsListening() - { - if (!IsListening) - { - throw new InvalidOperationException(nameof(ServerEndpointInfoSource.Start) + " method must be called before invoking this operation."); - } - } - - private bool IsListening => null != _server && null != _listenTask; - } -} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs index 6b11c75071..62391f376a 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs @@ -26,6 +26,11 @@ public DiagnosticsClient(int processId) : { } + internal DiagnosticsClient(IpcEndpointConfig config) : + this(new DiagnosticPortIpcEndpoint(config)) + { + } + internal DiagnosticsClient(IpcEndpoint endpoint) { _endpoint = endpoint; @@ -61,7 +66,7 @@ internal Task WaitForConnectionAsync(CancellationToken token) /// /// An EventPipeSession object representing the EventPipe session that just started. /// - public EventPipeSession StartEventPipeSession(IEnumerable providers, bool requestRundown=true, int circularBufferMB=256) + public EventPipeSession StartEventPipeSession(IEnumerable providers, bool requestRundown = true, int circularBufferMB = 256) { return new EventPipeSession(_endpoint, providers, requestRundown, circularBufferMB); } @@ -75,7 +80,7 @@ public EventPipeSession StartEventPipeSession(IEnumerable pro /// /// An EventPipeSession object representing the EventPipe session that just started. /// - public EventPipeSession StartEventPipeSession(EventPipeProvider provider, bool requestRundown=true, int circularBufferMB=256) + public EventPipeSession StartEventPipeSession(EventPipeProvider provider, bool requestRundown = true, int circularBufferMB = 256) { return new EventPipeSession(_endpoint, new[] { provider }, requestRundown, circularBufferMB); } @@ -86,12 +91,12 @@ public EventPipeSession StartEventPipeSession(EventPipeProvider provider, bool r /// 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) + public void WriteDump(DumpType dumpType, string dumpPath, bool logDumpGeneration = false) { if (string.IsNullOrEmpty(dumpPath)) throw new ArgumentNullException($"{nameof(dumpPath)} required"); - byte[] payload = SerializeCoreDump(dumpPath, dumpType, logDumpGeneration); + byte[] payload = SerializePayload(dumpPath, (uint)dumpType, logDumpGeneration); IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Dump, (byte)DumpCommandId.GenerateCoreDump, payload); IpcMessage response = IpcClient.SendMessage(_endpoint, message); switch ((DiagnosticsServerResponseId)response.Header.CommandId) @@ -117,7 +122,7 @@ public void WriteDump(DumpType dumpType, string dumpPath, bool logDumpGeneration /// 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) + public void AttachProfiler(TimeSpan attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData = null) { if (profilerGuid == null || profilerGuid == Guid.Empty) { @@ -129,7 +134,7 @@ public void AttachProfiler(TimeSpan attachTimeout, Guid profilerGuid, string pro throw new ArgumentException($"{nameof(profilerPath)} must be non-null"); } - byte[] serializedConfiguration = SerializeProfilerAttach((uint)attachTimeout.TotalSeconds, profilerGuid, profilerPath, additionalData); + byte[] serializedConfiguration = SerializePayload((uint)attachTimeout.TotalSeconds, profilerGuid, profilerPath, additionalData); var message = new IpcMessage(DiagnosticsServerCommandSet.Profiler, (byte)ProfilerCommandId.AttachProfiler, serializedConfiguration); var response = IpcClient.SendMessage(_endpoint, message); switch ((DiagnosticsServerResponseId)response.Header.CommandId) @@ -138,7 +143,7 @@ public void AttachProfiler(TimeSpan attachTimeout, Guid profilerGuid, string pro uint hr = BitConverter.ToUInt32(response.Payload, 0); if (hr == (uint)DiagnosticsIpcError.UnknownCommand) { - throw new UnsupportedCommandException("The target runtime does not support profiler attach"); + throw new UnsupportedCommandException("The target runtime does not support profiler attach"); } if (hr == (uint)DiagnosticsIpcError.ProfilerAlreadyActive) { @@ -156,35 +161,61 @@ public void AttachProfiler(TimeSpan attachTimeout, Guid profilerGuid, string pro // runtime timeout or respect attachTimeout as one total duration. } - internal void ResumeRuntime() + /// + /// Set a profiler as the startup profiler. It is only valid to issue this command + /// while the runtime is paused at startup. + /// + /// Guid for the profiler to be attached + /// Path to the profiler to be attached + public void SetStartupProfiler(Guid profilerGuid, string profilerPath) { - IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.ResumeRuntime); + if (profilerGuid == null || profilerGuid == Guid.Empty) + { + throw new ArgumentException($"{nameof(profilerGuid)} must be a valid Guid"); + } + + if (String.IsNullOrEmpty(profilerPath)) + { + throw new ArgumentException($"{nameof(profilerPath)} must be non-null"); + } + + byte[] serializedConfiguration = SerializePayload(profilerGuid, profilerPath); + var message = new IpcMessage(DiagnosticsServerCommandSet.Profiler, (byte)ProfilerCommandId.StartupProfiler, serializedConfiguration); var response = IpcClient.SendMessage(_endpoint, message); switch ((DiagnosticsServerResponseId)response.Header.CommandId) { case DiagnosticsServerResponseId.Error: - // Try fallback for Preview 7 and Preview 8 - ResumeRuntimeFallback(); - //var hr = BitConverter.ToInt32(response.Payload, 0); - //throw new ServerErrorException($"Resume runtime failed (HRESULT: 0x{hr:X8})"); - return; + uint hr = BitConverter.ToUInt32(response.Payload, 0); + if (hr == (uint)DiagnosticsIpcError.UnknownCommand) + { + throw new UnsupportedCommandException("The target runtime does not support the ProfilerStartup command."); + } + else if (hr == (uint)DiagnosticsIpcError.InvalidArgument) + { + throw new ServerErrorException("The runtime must be suspended to issue the SetStartupProfiler command."); + } + + throw new ServerErrorException($"Profiler startup failed (HRESULT: 0x{hr:X8})"); case DiagnosticsServerResponseId.OK: return; default: - throw new ServerErrorException($"Resume runtime failed - server responded with unknown command"); + throw new ServerErrorException($"Profiler startup failed - server responded with unknown command"); } } - // Fallback command for .NET 5 Preview 7 and Preview 8 - internal void ResumeRuntimeFallback() + /// + /// Tell the runtime to resume execution after being paused at startup. + /// + public void ResumeRuntime() { - IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Server, (byte)DiagnosticServerCommandId.ResumeRuntime); + IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.ResumeRuntime); var response = IpcClient.SendMessage(_endpoint, message); switch ((DiagnosticsServerResponseId)response.Header.CommandId) { case DiagnosticsServerResponseId.Error: - var hr = BitConverter.ToInt32(response.Payload, 0); - throw new ServerErrorException($"Resume runtime failed (HRESULT: 0x{hr:X8})"); + // Try fallback for Preview 7 and Preview 8 + ResumeRuntimeFallback(); + return; case DiagnosticsServerResponseId.OK: return; default: @@ -192,23 +223,43 @@ internal void ResumeRuntimeFallback() } } - internal ProcessInfo GetProcessInfo() + /// + /// Set an environment variable in the target process. + /// + /// The name of the environment variable to set. + /// The value of the environment variable to set. + public void SetEnvironmentVariable(string name, string value) { - IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessInfo); + if (String.IsNullOrEmpty(name)) + { + throw new ArgumentException($"{nameof(name)} must be non-null."); + } + + byte[] serializedConfiguration = SerializePayload(name, value); + var message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.SetEnvironmentVariable, serializedConfiguration); var response = IpcClient.SendMessage(_endpoint, message); switch ((DiagnosticsServerResponseId)response.Header.CommandId) { case DiagnosticsServerResponseId.Error: - var hr = BitConverter.ToInt32(response.Payload, 0); - throw new ServerErrorException($"Get process info failed (HRESULT: 0x{hr:X8})"); + uint hr = BitConverter.ToUInt32(response.Payload, 0); + if (hr == (uint)DiagnosticsIpcError.UnknownCommand) + { + throw new UnsupportedCommandException("The target runtime does not support the SetEnvironmentVariable command."); + } + + throw new ServerErrorException($"SetEnvironmentVariable failed (HRESULT: 0x{hr:X8})"); case DiagnosticsServerResponseId.OK: - return ProcessInfo.Parse(response.Payload); + return; default: - throw new ServerErrorException($"Get process info failed - server responded with unknown command"); + throw new ServerErrorException($"SetEnvironmentVariable failed - server responded with unknown command"); } } - public Dictionary GetProcessEnvironment() + /// + /// Gets all environement variables and their values from the target process. + /// + /// A dictionary containing all of the environment variables defined in the target process. + public Dictionary GetProcessEnvironment() { var message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessEnvironment); Stream continuation = IpcClient.SendMessage(_endpoint, message, out IpcMessage response); @@ -219,7 +270,7 @@ public Dictionary GetProcessEnvironment() throw new ServerErrorException($"Get process environment failed (HRESULT: 0x{hr:X8})"); case DiagnosticsServerResponseId.OK: ProcessEnvironmentHelper helper = ProcessEnvironmentHelper.Parse(response.Payload); - Task> envTask = helper.ReadEnvironmentAsync(continuation); + Task> envTask = helper.ReadEnvironmentAsync(continuation); envTask.Wait(); return envTask.Result; default: @@ -253,42 +304,156 @@ static IEnumerable GetAllPublishedProcesses() return GetAllPublishedProcesses().Distinct(); } - private static byte[] SerializeCoreDump(string dumpName, DumpType dumpType, bool diagnostics) + + // Fallback command for .NET 5 Preview 7 and Preview 8 + internal void ResumeRuntimeFallback() + { + IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Server, (byte)DiagnosticServerCommandId.ResumeRuntime); + var response = IpcClient.SendMessage(_endpoint, message); + switch ((DiagnosticsServerResponseId)response.Header.CommandId) + { + case DiagnosticsServerResponseId.Error: + var hr = BitConverter.ToUInt32(response.Payload, 0); + if (hr == (uint)DiagnosticsIpcError.UnknownCommand) + { + throw new UnsupportedCommandException($"Resume runtime command is unknown by target runtime."); + } + throw new ServerErrorException($"Resume runtime failed (HRESULT: 0x{hr:X8})"); + case DiagnosticsServerResponseId.OK: + return; + default: + throw new ServerErrorException($"Resume runtime failed - server responded with unknown command"); + } + } + + internal ProcessInfo GetProcessInfo() + { + // RE: https://github.com/dotnet/runtime/issues/54083 + // If the GetProcessInfo2 command is sent too early, it will crash the runtime instance. + // Disable the usage of the command until that issue is fixed. + + // Attempt to get ProcessInfo v2 + //ProcessInfo processInfo = GetProcessInfo2(); + //if (null != processInfo) + //{ + // return processInfo; + //} + + // Attempt to get ProcessInfo v1 + IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessInfo); + var response = IpcClient.SendMessage(_endpoint, message); + switch ((DiagnosticsServerResponseId)response.Header.CommandId) + { + case DiagnosticsServerResponseId.Error: + var hr = BitConverter.ToInt32(response.Payload, 0); + throw new ServerErrorException($"Get process info failed (HRESULT: 0x{hr:X8})"); + case DiagnosticsServerResponseId.OK: + return ProcessInfo.ParseV1(response.Payload); + default: + throw new ServerErrorException($"Get process info failed - server responded with unknown command"); + } + } + + private ProcessInfo GetProcessInfo2() + { + IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessInfo2); + var response = IpcClient.SendMessage(_endpoint, message); + switch ((DiagnosticsServerResponseId)response.Header.CommandId) + { + case DiagnosticsServerResponseId.Error: + uint hr = BitConverter.ToUInt32(response.Payload, 0); + // In the case that the runtime doesn't understand the GetProcessInfo2 command, + // just break to allow fallback to try to get ProcessInfo v1. + if (hr == (uint)DiagnosticsIpcError.UnknownCommand) + { + return null; + } + throw new ServerErrorException($"GetProcessInfo2 failed (HRESULT: 0x{hr:X8})"); + case DiagnosticsServerResponseId.OK: + return ProcessInfo.ParseV2(response.Payload); + default: + throw new ServerErrorException($"Get process info failed - server responded with unknown command"); + } + } + + private static byte[] SerializePayload(T arg) { using (var stream = new MemoryStream()) using (var writer = new BinaryWriter(stream)) { - writer.WriteString(dumpName); - writer.Write((uint)dumpType); - writer.Write((uint)(diagnostics ? 1 : 0)); + SerializePayloadArgument(arg, writer); writer.Flush(); return stream.ToArray(); } } - private static byte[] SerializeProfilerAttach(uint attachTimeout, Guid profilerGuid, string profilerPath, byte[] additionalData) + private static byte[] SerializePayload(T1 arg1, T2 arg2) { using (var stream = new MemoryStream()) using (var writer = new BinaryWriter(stream)) { - writer.Write(attachTimeout); - writer.Write(profilerGuid.ToByteArray()); - writer.WriteString(profilerPath); + SerializePayloadArgument(arg1, writer); + SerializePayloadArgument(arg2, writer); - if (additionalData == null) - { - writer.Write(0); - } - else - { - writer.Write(additionalData.Length); - writer.Write(additionalData); - } + writer.Flush(); + return stream.ToArray(); + } + } + + private static byte[] SerializePayload(T1 arg1, T2 arg2, T3 arg3) + { + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + { + SerializePayloadArgument(arg1, writer); + SerializePayloadArgument(arg2, writer); + SerializePayloadArgument(arg3, writer); + + writer.Flush(); + return stream.ToArray(); + } + } + + private static byte[] SerializePayload(T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) + { + SerializePayloadArgument(arg1, writer); + SerializePayloadArgument(arg2, writer); + SerializePayloadArgument(arg3, writer); + SerializePayloadArgument(arg4, writer); writer.Flush(); return stream.ToArray(); } } + + private static void SerializePayloadArgument(T obj, BinaryWriter writer) + { + if (typeof(T) == typeof(string)) + { + writer.WriteString((string)((object)obj)); + } + else if (typeof(T) == typeof(int)) + { + writer.Write((int)((object)obj)); + } + else if (typeof(T) == typeof(uint)) + { + writer.Write((uint)((object)obj)); + } + else if (typeof(T) == typeof(bool)) + { + bool bValue = (bool)((object)obj); + uint uiValue = bValue ? (uint)1 : 0; + writer.Write(uiValue); + } + else + { + throw new ArgumentException($"Type {obj.GetType()} is not supported in SerializePayloadArgument, please add it."); + } + } } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs index ca59ba2558..7b22f00c04 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs @@ -51,6 +51,7 @@ internal enum DumpCommandId : byte internal enum ProfilerCommandId : byte { AttachProfiler = 0x01, + StartupProfiler = 0x02, } internal enum ProcessCommandId : byte @@ -58,5 +59,7 @@ internal enum ProcessCommandId : byte GetProcessInfo = 0x00, ResumeRuntime = 0x01, GetProcessEnvironment = 0x02, + SetEnvironmentVariable = 0x03, + GetProcessInfo2 = 0x04 } } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcEndpointConfig.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcEndpointConfig.cs new file mode 100644 index 0000000000..867cba276e --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcEndpointConfig.cs @@ -0,0 +1,182 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal class IpcEndpointConfig + { + public enum PortType + { + Connect, + Listen + } + + public enum TransportType + { + NamedPipe, + UnixDomainSocket + } + + PortType _portType; + + TransportType _transportType; + + public string Address { get; } + + public bool IsConnectConfig => _portType == PortType.Connect; + + public bool IsListenConfig => _portType == PortType.Listen; + + public TransportType Transport => _transportType; + + const string NamedPipeSchema = "namedpipe"; + const string UnixDomainSocketSchema = "uds"; + const string NamedPipeDefaultIPCRoot = @"\\.\pipe\"; + const string NamedPipeSchemaDefaultIPCRootPath = "/pipe/"; + + public IpcEndpointConfig(string address, TransportType transportType, PortType portType) + { + if (string.IsNullOrEmpty(address)) + throw new ArgumentException("Address is null or empty."); + + switch (transportType) + { + case TransportType.NamedPipe: + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new PlatformNotSupportedException($"{NamedPipeSchema} is only supported on Windows."); + break; + } + case TransportType.UnixDomainSocket: + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new PlatformNotSupportedException($"{UnixDomainSocketSchema} is not supported on Windows, use {NamedPipeSchema}."); + break; + } + default: + { + throw new NotSupportedException($"{transportType} not supported."); + } + } + + Address = address; + _transportType = transportType; + _portType = portType; + } + + // Config format: [Address],[PortType] + // + // Address in UnixDomainSocket formats: + // myport => myport + // uds:myport => myport + // /User/mrx/myport.sock => /User/mrx/myport.sock + // uds:/User/mrx/myport.sock => /User/mrx/myport.sock + // uds://authority/User/mrx/myport.sock => /User/mrx/myport.sock + // uds:///User/mrx/myport.sock => /User/mrx/myport.sock + // + // Address in NamedPipe formats: + // myport => myport + // namedpipe:myport => myport + // \\.\pipe\myport => myport (dropping \\.\pipe\ is inline with implemented namedpipe client/server) + // namedpipe://./pipe/myport => myport (dropping authority and /pipe/ is inline with implemented namedpipe client/server) + // namedpipe:/pipe/myport => myport (dropping /pipe/ is inline with implemented namedpipe client/server) + // namedpipe://authority/myport => myport + // namedpipe:///myport => myport + // + // PortType: Listen|Connect, default Listen. + public static bool TryParse(string config, out IpcEndpointConfig result) + { + try + { + result = Parse(config); + } + catch(Exception) + { + result = null; + } + return result != null; + } + + public static IpcEndpointConfig Parse(string config) + { + if (string.IsNullOrEmpty(config)) + throw new FormatException("Missing IPC endpoint config."); + + string address = ""; + PortType portType = PortType.Connect; + TransportType transportType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? TransportType.NamedPipe : TransportType.UnixDomainSocket; + + if (!string.IsNullOrEmpty(config)) + { + var parts = config.Split(','); + if (parts.Length > 2) + throw new FormatException($"Unknow IPC endpoint config format, {config}."); + + if (string.IsNullOrEmpty(parts[0])) + throw new FormatException($"Missing IPC endpoint config address, {config}."); + + portType = PortType.Listen; + address = parts[0]; + + if (parts.Length == 2) + { + if (string.Equals(parts[1], "connect", StringComparison.OrdinalIgnoreCase)) + { + portType = PortType.Connect; + } + else if (string.Equals(parts[1], "listen", StringComparison.OrdinalIgnoreCase)) + { + portType = PortType.Listen; + } + else + { + throw new FormatException($"Unknow IPC endpoint config keyword, {parts[1]} in {config}."); + } + } + } + + if (Uri.TryCreate(address, UriKind.Absolute, out Uri parsedAddress)) + { + if (string.Equals(parsedAddress.Scheme, NamedPipeSchema, StringComparison.OrdinalIgnoreCase)) + { + transportType = TransportType.NamedPipe; + address = parsedAddress.AbsolutePath; + } + else if (string.Equals(parsedAddress.Scheme, UnixDomainSocketSchema, StringComparison.OrdinalIgnoreCase)) + { + transportType = TransportType.UnixDomainSocket; + address = parsedAddress.AbsolutePath; + } + else if (string.Equals(parsedAddress.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase)) + { + address = parsedAddress.AbsolutePath; + } + else if (!string.IsNullOrEmpty(parsedAddress.Scheme)) + { + throw new FormatException($"{parsedAddress.Scheme} not supported."); + } + } + else + { + if (address.StartsWith(NamedPipeDefaultIPCRoot, StringComparison.OrdinalIgnoreCase)) + transportType = TransportType.NamedPipe; + } + + if (transportType == TransportType.NamedPipe) + { + if (address.StartsWith(NamedPipeDefaultIPCRoot, StringComparison.OrdinalIgnoreCase)) + address = address.Substring(NamedPipeDefaultIPCRoot.Length); + else if (address.StartsWith(NamedPipeSchemaDefaultIPCRootPath, StringComparison.OrdinalIgnoreCase)) + address = address.Substring(NamedPipeSchemaDefaultIPCRootPath.Length); + else if (address.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + address = address.Substring("/".Length); + } + + return new IpcEndpointConfig(address, transportType, portType); + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs index 372d49ef3d..00b50980ac 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcMessage.cs @@ -15,11 +15,12 @@ namespace Microsoft.Diagnostics.NETCore.Client /// internal enum DiagnosticsIpcError : uint { + InvalidArgument = 0x80070057, ProfilerAlreadyActive = 0x8013136A, BadEncoding = 0x80131384, UnknownCommand = 0x80131385, UnknownMagic = 0x80131386, - UnknownError = 0x80131387 + UnknownError = 0x80131387, } /// diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs index 77715c5aa6..150459037f 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcTransport.cs @@ -48,6 +48,59 @@ internal abstract class IpcEndpoint public abstract Task WaitForConnectionAsync(CancellationToken token); } + internal class IpcEndpointHelper + { + public static Stream Connect(IpcEndpointConfig config, TimeSpan timeout) + { + if (config.Transport == IpcEndpointConfig.TransportType.NamedPipe) + { + var namedPipe = new NamedPipeClientStream( + ".", + config.Address, + PipeDirection.InOut, + PipeOptions.None, + TokenImpersonationLevel.Impersonation); + namedPipe.Connect((int)timeout.TotalMilliseconds); + return namedPipe; + } + else if (config.Transport == IpcEndpointConfig.TransportType.UnixDomainSocket) + { + var socket = new IpcUnixDomainSocket(); + socket.Connect(new IpcUnixDomainSocketEndPoint(config.Address), timeout); + return new ExposedSocketNetworkStream(socket, ownsSocket: true); + } + else + { + throw new ArgumentException($"Unsupported IpcEndpointConfig transport type {config.Transport}"); + } + } + + public static async Task ConnectAsync(IpcEndpointConfig config, CancellationToken token) + { + if (config.Transport == IpcEndpointConfig.TransportType.NamedPipe) + { + var namedPipe = new NamedPipeClientStream( + ".", + config.Address, + PipeDirection.InOut, + PipeOptions.None, + TokenImpersonationLevel.Impersonation); + await namedPipe.ConnectAsync(token).ConfigureAwait(false); + return namedPipe; + } + else if (config.Transport == IpcEndpointConfig.TransportType.UnixDomainSocket) + { + var socket = new IpcUnixDomainSocket(); + await socket.ConnectAsync(new IpcUnixDomainSocketEndPoint(config.Address), token).ConfigureAwait(false); + return new ExposedSocketNetworkStream(socket, ownsSocket: true); + } + else + { + throw new ArgumentException($"Unsupported IpcEndpointConfig transport type {config.Transport}"); + } + } + } + internal class ServerIpcEndpoint : IpcEndpoint { private readonly Guid _runtimeId; @@ -100,12 +153,64 @@ public override int GetHashCode() } } + internal class DiagnosticPortIpcEndpoint : IpcEndpoint + { + IpcEndpointConfig _config; + + public DiagnosticPortIpcEndpoint(string diagnosticPort) + { + _config = IpcEndpointConfig.Parse(diagnosticPort); + } + + public DiagnosticPortIpcEndpoint(IpcEndpointConfig config) + { + _config = config; + } + + public override Stream Connect(TimeSpan timeout) + { + return IpcEndpointHelper.Connect(_config, timeout); + } + + public override async Task ConnectAsync(CancellationToken token) + { + return await IpcEndpointHelper.ConnectAsync(_config, token).ConfigureAwait(false); + } + + public override void WaitForConnection(TimeSpan timeout) + { + using var _ = Connect(timeout); + } + + public override async Task WaitForConnectionAsync(CancellationToken token) + { + using var _ = await ConnectAsync(token).ConfigureAwait(false); + } + + public override bool Equals(object obj) + { + return Equals(obj as DiagnosticPortIpcEndpoint); + } + + public bool Equals(DiagnosticPortIpcEndpoint other) + { + return other != null && other._config == _config; + } + + public override int GetHashCode() + { + return _config.GetHashCode(); + } + } + internal class PidIpcEndpoint : IpcEndpoint { 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 int _pid; + int _pid; + + IpcEndpointConfig _config; /// /// Creates a reference to a .NET process's IPC Transport @@ -121,45 +226,15 @@ public PidIpcEndpoint(int pid) public override Stream Connect(TimeSpan timeout) { string address = GetDefaultAddress(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var namedPipe = new NamedPipeClientStream( - ".", - address, - PipeDirection.InOut, - PipeOptions.None, - TokenImpersonationLevel.Impersonation); - namedPipe.Connect((int)timeout.TotalMilliseconds); - return namedPipe; - } - else - { - var socket = new IpcUnixDomainSocket(); - socket.Connect(new IpcUnixDomainSocketEndPoint(Path.Combine(IpcRootPath, address)), timeout); - return new ExposedSocketNetworkStream(socket, ownsSocket: true); - } + _config = IpcEndpointConfig.Parse(address + ",connect"); + return IpcEndpointHelper.Connect(_config, timeout); } public override async Task ConnectAsync(CancellationToken token) { string address = GetDefaultAddress(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var namedPipe = new NamedPipeClientStream( - ".", - address, - PipeDirection.InOut, - PipeOptions.None, - TokenImpersonationLevel.Impersonation); - await namedPipe.ConnectAsync(token).ConfigureAwait(false); - return namedPipe; - } - else - { - var socket = new IpcUnixDomainSocket(); - await socket.ConnectAsync(new IpcUnixDomainSocketEndPoint(Path.Combine(IpcRootPath, address)), token).ConfigureAwait(false); - return new ExposedSocketNetworkStream(socket, ownsSocket: true); - } + _config = IpcEndpointConfig.Parse(address + ",connect"); + return await IpcEndpointHelper.ConnectAsync(_config, token).ConfigureAwait(false); } public override void WaitForConnection(TimeSpan timeout) diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs index 318e1d96fc..b3985e49c4 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessEnvironment.cs @@ -42,7 +42,7 @@ public static ProcessEnvironmentHelper Parse(byte[] payload) cursor += sizeof(UInt32); while (cursor < envBlock.Length) { - string pair = ReadString(envBlock, ref cursor); + string pair = IpcHelpers.ReadString(envBlock, ref cursor); int equalsIdx = pair.IndexOf('='); env[pair.Substring(0,equalsIdx)] = equalsIdx != pair.Length - 1 ? pair.Substring(equalsIdx+1) : ""; } @@ -50,18 +50,6 @@ public static ProcessEnvironmentHelper Parse(byte[] payload) return env; } - private static string ReadString(byte[] buffer, ref int index) - { - // Length of the string of UTF-16 characters - int length = (int)BitConverter.ToUInt32(buffer, index); - index += sizeof(UInt32); - - int size = (int)length * sizeof(char); - // The string contains an ending null character; remove it before returning the value - string value = Encoding.Unicode.GetString(buffer, index, size).Substring(0, length - 1); - index += size; - return value; - } private UInt32 ExpectedSizeInBytes { get; set; } private UInt16 Future { get; set; } diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs index 91bec0c7c9..887612d866 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/ProcessInfo.cs @@ -17,6 +17,18 @@ namespace Microsoft.Diagnostics.NETCore.Client * # bytes - Operating system string length and data * # bytes - Process architecture string length and data * + * ==ProcessInfo2== + * The response payload to issuing the GetProcessInfo2 command. + * + * 8 bytes - PID (little-endian) + * 16 bytes - CLR Runtime Instance Cookie (little-endian) + * # bytes - Command line string length and data + * # bytes - Operating system string length and data + * # bytes - Process architecture string length and data + * # bytes - Managed entrypoint assembly name + * # bytes - CLR product version string (may include prerelease labels) + * + * * The "string length and data" fields are variable length: * 4 bytes - Length of string data in UTF-16 characters * (2 * length) bytes - The data of the string encoded using Unicode @@ -27,11 +39,33 @@ internal class ProcessInfo { private static readonly int GuidSizeInBytes = 16; - public static ProcessInfo Parse(byte[] payload) + /// + /// Parses a ProcessInfo payload. + /// + internal static ProcessInfo ParseV1(byte[] payload) { - ProcessInfo processInfo = new ProcessInfo(); + int index = 0; + return ParseCommon(payload, ref index); + } + /// + /// Parses a ProcessInfo2 payload. + /// + internal static ProcessInfo ParseV2(byte[] payload) + { int index = 0; + ProcessInfo processInfo = ParseCommon(payload, ref index); + + processInfo.ManagedEntrypointAssemblyName = IpcHelpers.ReadString(payload, ref index); + processInfo.ClrProductVersionString = IpcHelpers.ReadString(payload, ref index); + + return processInfo; + } + + private static ProcessInfo ParseCommon(byte[] payload, ref int index) + { + ProcessInfo processInfo = new ProcessInfo(); + processInfo.ProcessId = BitConverter.ToUInt64(payload, index); index += sizeof(UInt64); @@ -40,30 +74,19 @@ public static ProcessInfo Parse(byte[] payload) processInfo.RuntimeInstanceCookie = new Guid(cookieBuffer); index += GuidSizeInBytes; - processInfo.CommandLine = ReadString(payload, ref index); - processInfo.OperatingSystem = ReadString(payload, ref index); - processInfo.ProcessArchitecture = ReadString(payload, ref index); + processInfo.CommandLine = IpcHelpers.ReadString(payload, ref index); + processInfo.OperatingSystem = IpcHelpers.ReadString(payload, ref index); + processInfo.ProcessArchitecture = IpcHelpers.ReadString(payload, ref index); return processInfo; } - private static string ReadString(byte[] buffer, ref int index) - { - // Length of the string of UTF-16 characters - int length = (int)BitConverter.ToUInt32(buffer, index); - index += sizeof(UInt32); - - int size = (int)length * sizeof(char); - // The string contains an ending null character; remove it before returning the value - string value = Encoding.Unicode.GetString(buffer, index, size).Substring(0, length - 1); - index += size; - return value; - } - public UInt64 ProcessId { get; private set; } public Guid RuntimeInstanceCookie { get; private set; } public string CommandLine { get; private set; } public string OperatingSystem { get; private set; } public string ProcessArchitecture { get; private set; } + public string ManagedEntrypointAssemblyName { get; private set; } + public string ClrProductVersionString { get; private set; } } } \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs index 9f36ba0520..88d0c1558b 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterFactory.cs @@ -44,7 +44,7 @@ internal class DiagnosticsServerRouterFactory public virtual ILogger Logger { get; } - public virtual void Start() + public virtual Task Start(CancellationToken token) { throw new NotImplementedException(); } @@ -150,7 +150,7 @@ protected bool IsCompletedSuccessfully(Task t) /// internal class TcpServerRouterFactory : IIpcServerTransportCallbackInternal { - readonly ILogger _logger; + protected readonly ILogger _logger; string _tcpServerAddress; @@ -177,6 +177,13 @@ public string TcpServerAddress get { return _tcpServerAddress; } } + public delegate TcpServerRouterFactory CreateInstanceDelegate(string tcpServer, int runtimeTimeoutMs, ILogger logger); + + public static TcpServerRouterFactory CreateDefaultInstance(string tcpServer, int runtimeTimeoutMs, ILogger logger) + { + return new TcpServerRouterFactory(tcpServer, runtimeTimeoutMs, logger); + } + public TcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger logger) { _logger = logger; @@ -192,12 +199,12 @@ public TcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger lo _tcpServer.TransportCallback = this; } - public void Start() + public virtual void Start() { _tcpServer.Start(); } - public async Task Stop() + public virtual async Task Stop() { await _tcpServer.DisposeAsync().ConfigureAwait(false); } @@ -281,15 +288,22 @@ public void CreatedNewServer(EndPoint localEP) /// internal class TcpClientRouterFactory { - readonly ILogger _logger; + protected readonly ILogger _logger; - readonly string _tcpClientAddress; + protected readonly string _tcpClientAddress; - bool _auto_shutdown; + protected bool _auto_shutdown; + + protected int TcpClientTimeoutMs { get; set; } = Timeout.Infinite; - int TcpClientTimeoutMs { get; set; } = Timeout.Infinite; + protected int TcpClientRetryTimeoutMs { get; set; } = 500; - int TcpClientRetryTimeoutMs { get; set; } = 500; + public delegate TcpClientRouterFactory CreateInstanceDelegate(string tcpClient, int runtimeTimeoutMs, ILogger logger); + + public static TcpClientRouterFactory CreateDefaultInstance(string tcpClient, int runtimeTimeoutMs, ILogger logger) + { + return new TcpClientRouterFactory(tcpClient, runtimeTimeoutMs, logger); + } public string TcpClientAddress { get { return _tcpClientAddress; } @@ -304,13 +318,30 @@ public TcpClientRouterFactory(string tcpClient, int runtimeTimeoutMs, ILogger lo TcpClientTimeoutMs = runtimeTimeoutMs; } - public async Task ConnectTcpStreamAsync(CancellationToken token) + public virtual async Task ConnectTcpStreamAsync(CancellationToken token) + { + return await ConnectTcpStreamAsyncInternal(token, _auto_shutdown).ConfigureAwait(false); + } + + public virtual async Task ConnectTcpStreamAsync(CancellationToken token, bool retry) + { + return await ConnectTcpStreamAsyncInternal(token, retry).ConfigureAwait(false); + } + + public virtual void Start() + { + } + + public virtual void Stop() + { + } + + async Task ConnectTcpStreamAsyncInternal(CancellationToken token, bool retry) { Stream tcpClientStream = null; _logger?.LogDebug($"Connecting new tcp endpoint \"{_tcpClientAddress}\"."); - bool retry = false; IpcTcpSocketEndPoint clientTcpEndPoint = new IpcTcpSocketEndPoint(_tcpClientAddress); Socket clientSocket = null; @@ -325,7 +356,7 @@ public async Task ConnectTcpStreamAsync(CancellationToken token) try { - await ConnectAsyncInternal(clientSocket, clientTcpEndPoint, token).ConfigureAwait(false); + await ConnectAsyncInternal(clientSocket, clientTcpEndPoint, connectTokenSource.Token).ConfigureAwait(false); retry = false; } catch (Exception) @@ -342,10 +373,10 @@ public async Task ConnectTcpStreamAsync(CancellationToken token) throw new TimeoutException(); } - // If we are not doing auto shutdown when runtime is unavailable, fail right away, this will + // If we are not doing retries when runtime is unavailable, fail right away, this will // break any accepted IPC connections, making sure client is notified and could reconnect. - // If we do have auto shutdown enabled, retry until succeed or time out. - if (!_auto_shutdown) + // If not, retry until succeed or time out. + if (!retry) { _logger?.LogTrace($"Failed connecting {_tcpClientAddress}."); throw; @@ -356,8 +387,6 @@ public async Task ConnectTcpStreamAsync(CancellationToken token) // If we get an error (without hitting timeout above), most likely due to unavailable listener. // Delay execution to prevent to rapid retry attempts. await Task.Delay(TcpClientRetryTimeoutMs, token).ConfigureAwait(false); - - retry = true; } } while (retry); @@ -411,10 +440,11 @@ public string IpcServerPath { public IpcServerRouterFactory(string ipcServer, ILogger logger) { + if (string.IsNullOrEmpty(ipcServer)) + throw new ArgumentException("Missing IPC server path."); + _logger = logger; _ipcServerPath = ipcServer; - if (string.IsNullOrEmpty(_ipcServerPath)) - _ipcServerPath = GetDefaultIpcServerPath(); _ipcServer = IpcServerTransport.Create(_ipcServerPath, IpcServerTransport.MaxAllowedConnections, false); } @@ -461,26 +491,6 @@ public async Task AcceptIpcStreamAsync(CancellationToken token) return ipcServerStream; } - - static string GetDefaultIpcServerPath() - { - int processId = Process.GetCurrentProcess().Id; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}"); - } - else - { - DateTime unixEpoch; -#if NETCOREAPP2_1_OR_GREATER - unixEpoch = DateTime.UnixEpoch; -#else - unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); -#endif - TimeSpan diff = Process.GetCurrentProcess().StartTime.ToUniversalTime() - unixEpoch; - return Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-{(long)diff.TotalSeconds}-socket"); - } - } } /// @@ -597,10 +607,10 @@ internal class IpcServerTcpServerRouterFactory : DiagnosticsServerRouterFactory TcpServerRouterFactory _tcpServerRouterFactory; IpcServerRouterFactory _ipcServerRouterFactory; - public IpcServerTcpServerRouterFactory(string ipcServer, string tcpServer, int runtimeTimeoutMs, ILogger logger) + public IpcServerTcpServerRouterFactory(string ipcServer, string tcpServer, int runtimeTimeoutMs, TcpServerRouterFactory.CreateInstanceDelegate factory, ILogger logger) { _logger = logger; - _tcpServerRouterFactory = new TcpServerRouterFactory(tcpServer, runtimeTimeoutMs, logger); + _tcpServerRouterFactory = factory(tcpServer, runtimeTimeoutMs, logger); _ipcServerRouterFactory = new IpcServerRouterFactory(ipcServer, logger); } @@ -628,16 +638,19 @@ public override ILogger Logger } } - public override void Start() + public override Task Start(CancellationToken token) { _tcpServerRouterFactory.Start(); _ipcServerRouterFactory.Start(); _logger?.LogInformation($"Starting IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> TCP server ({_tcpServerRouterFactory.TcpServerAddress}) router."); + + return Task.CompletedTask; } public override Task Stop() { + _logger?.LogInformation($"Stopping IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> TCP server ({_tcpServerRouterFactory.TcpServerAddress}) router."); _ipcServerRouterFactory.Stop(); return _tcpServerRouterFactory.Stop(); } @@ -786,11 +799,11 @@ internal class IpcServerTcpClientRouterFactory : DiagnosticsServerRouterFactory IpcServerRouterFactory _ipcServerRouterFactory; TcpClientRouterFactory _tcpClientRouterFactory; - public IpcServerTcpClientRouterFactory(string ipcServer, string tcpClient, int runtimeTimeoutMs, ILogger logger) + public IpcServerTcpClientRouterFactory(string ipcServer, string tcpClient, int runtimeTimeoutMs, TcpClientRouterFactory.CreateInstanceDelegate factory, ILogger logger) { _logger = logger; _ipcServerRouterFactory = new IpcServerRouterFactory(ipcServer, logger); - _tcpClientRouterFactory = new TcpClientRouterFactory(tcpClient, runtimeTimeoutMs, logger); + _tcpClientRouterFactory = factory(tcpClient, runtimeTimeoutMs, logger); } public override string IpcAddress @@ -817,14 +830,19 @@ public override ILogger Logger } } - public override void Start() + public override Task Start(CancellationToken token) { _ipcServerRouterFactory.Start(); + _tcpClientRouterFactory.Start(); _logger?.LogInformation($"Starting IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> TCP client ({_tcpClientRouterFactory.TcpClientAddress}) router."); + + return Task.CompletedTask; } public override Task Stop() { + _logger?.LogInformation($"Stopping IPC server ({_ipcServerRouterFactory.IpcServerPath}) <--> TCP client ({_tcpClientRouterFactory.TcpClientAddress}) router."); + _tcpClientRouterFactory.Stop(); _ipcServerRouterFactory.Stop(); return Task.CompletedTask; } @@ -905,11 +923,11 @@ internal class IpcClientTcpServerRouterFactory : DiagnosticsServerRouterFactory IpcClientRouterFactory _ipcClientRouterFactory; TcpServerRouterFactory _tcpServerRouterFactory; - public IpcClientTcpServerRouterFactory(string ipcClient, string tcpServer, int runtimeTimeoutMs, ILogger logger) + public IpcClientTcpServerRouterFactory(string ipcClient, string tcpServer, int runtimeTimeoutMs, TcpServerRouterFactory.CreateInstanceDelegate factory, ILogger logger) { _logger = logger; _ipcClientRouterFactory = new IpcClientRouterFactory(ipcClient, logger); - _tcpServerRouterFactory = new TcpServerRouterFactory(tcpServer, runtimeTimeoutMs, logger); + _tcpServerRouterFactory = factory(tcpServer, runtimeTimeoutMs, logger); } public override string IpcAddress @@ -936,14 +954,21 @@ public override ILogger Logger } } - public override void Start() + public override Task Start(CancellationToken token) { + if (string.IsNullOrEmpty(_ipcClientRouterFactory.IpcClientPath)) + throw new ArgumentException("No IPC client path specified."); + _tcpServerRouterFactory.Start(); + _logger?.LogInformation($"Starting IPC client ({_ipcClientRouterFactory.IpcClientPath}) <--> TCP server ({_tcpServerRouterFactory.TcpServerAddress}) router."); + + return Task.CompletedTask; } public override Task Stop() { + _logger?.LogInformation($"Stopping IPC client ({_ipcClientRouterFactory.IpcClientPath}) <--> TCP server ({_tcpServerRouterFactory.TcpServerAddress}) router."); return _tcpServerRouterFactory.Stop(); } @@ -1002,8 +1027,8 @@ public override async Task CreateRouterAsync(CancellationToken token) try { - // TcpServer consumes advertise message, needs to be replayed back to ipc client stream. Use router process ID as representation. - await IpcAdvertise.SerializeAsync(ipcClientStream, _tcpServerRouterFactory.RuntimeInstanceId, (ulong)Process.GetCurrentProcess().Id, token).ConfigureAwait(false); + // TcpServer consumes advertise message, needs to be replayed back to ipc client. + await IpcAdvertise.SerializeAsync(ipcClientStream, _tcpServerRouterFactory.RuntimeInstanceId, (ulong)_tcpServerRouterFactory.RuntimeProcessId, token).ConfigureAwait(false); } catch (Exception) { @@ -1029,6 +1054,178 @@ public override async Task CreateRouterAsync(CancellationToken token) } } + /// + /// This class creates IPC Client - TCP Client router instances. + /// Supports NamedPipes/UnixDomainSocket client and TCP/IP client. + /// + internal class IpcClientTcpClientRouterFactory : DiagnosticsServerRouterFactory + { + Guid _runtimeInstanceId; + ulong _runtimeProcessId; + ILogger _logger; + IpcClientRouterFactory _ipcClientRouterFactory; + TcpClientRouterFactory _tcpClientRouterFactory; + + public IpcClientTcpClientRouterFactory(string ipcClient, string tcpClient, int runtimeTimeoutMs, TcpClientRouterFactory.CreateInstanceDelegate factory, ILogger logger) + { + _runtimeInstanceId = Guid.Empty; + _runtimeProcessId = 0; + _logger = logger; + _ipcClientRouterFactory = new IpcClientRouterFactory(ipcClient, logger); + _tcpClientRouterFactory = factory(tcpClient, runtimeTimeoutMs, logger); + } + + public override string IpcAddress { + get + { + return _ipcClientRouterFactory.IpcClientPath; + } + } + + public override string TcpAddress { + get + { + return _tcpClientRouterFactory.TcpClientAddress; + } + } + + public override ILogger Logger { + get + { + return _logger; + } + } + + public override Task Start(CancellationToken token) + { + _tcpClientRouterFactory.Start(); + _logger?.LogInformation($"Starting IPC client ({_ipcClientRouterFactory.IpcClientPath}) <--> TCP client ({_tcpClientRouterFactory.TcpClientAddress}) router."); + + var requestRuntimeInfo = new Task(() => + { + try + { + _logger?.LogDebug($"Requesting runtime process information."); + + // Get new tcp client endpoint. + using var tcpClientStream = _tcpClientRouterFactory.ConnectTcpStreamAsync(token).Result; + + // Request process info. + IpcMessage message = new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.GetProcessInfo); + + byte[] buffer = message.Serialize(); + tcpClientStream.Write(buffer, 0, buffer.Length); + + var response = IpcMessage.Parse(tcpClientStream); + if ((DiagnosticsServerResponseId)response.Header.CommandId == DiagnosticsServerResponseId.OK) + { + var info = ProcessInfo.ParseV1(response.Payload); + + _runtimeProcessId = info.ProcessId; + _runtimeInstanceId = info.RuntimeInstanceCookie; + + _logger?.LogDebug($"Retrieved runtime process information, pid={_runtimeProcessId}, cookie={_runtimeInstanceId}."); + } + else + { + throw new ServerErrorException("Failed to retrieve runtime process info."); + } + } + catch (Exception) + { + _runtimeProcessId = (ulong)Process.GetCurrentProcess().Id; + _runtimeInstanceId = Guid.NewGuid(); + _logger?.LogWarning($"Failed to retrieve runtime process info, fallback to current process information, pid={_runtimeProcessId}, cookie={_runtimeInstanceId}."); + } + }); + + requestRuntimeInfo.Start(); + return requestRuntimeInfo; + } + + public override Task Stop() + { + _logger?.LogInformation($"Stopping IPC client ({_ipcClientRouterFactory.IpcClientPath}) <--> TCP client ({_tcpClientRouterFactory.TcpClientAddress}) router."); + _tcpClientRouterFactory.Stop(); + return Task.CompletedTask; + } + + public override async Task CreateRouterAsync(CancellationToken token) + { + Stream tcpClientStream = null; + Stream ipcClientStream = null; + + _logger?.LogDebug("Trying to create a new router instance."); + + try + { + using CancellationTokenSource cancelRouter = CancellationTokenSource.CreateLinkedTokenSource(token); + + // Get new tcp client endpoint. + tcpClientStream = await _tcpClientRouterFactory.ConnectTcpStreamAsync(cancelRouter.Token, true).ConfigureAwait(false); + + // Get new ipc client endpoint. + using var ipcClientStreamTask = _ipcClientRouterFactory.ConnectIpcStreamAsync(cancelRouter.Token); + + // We have a valid tcp stream and a pending ipc stream. Wait for completion + // or disconnect of tcp stream. + using var checkTcpStreamTask = IsStreamConnectedAsync(tcpClientStream, cancelRouter.Token); + + // Wait for at least completion of one task. + await Task.WhenAny(ipcClientStreamTask, checkTcpStreamTask).ConfigureAwait(false); + + // Cancel out any pending tasks not yet completed. + cancelRouter.Cancel(); + + try + { + await Task.WhenAll(ipcClientStreamTask, checkTcpStreamTask).ConfigureAwait(false); + } + catch (Exception) + { + // Check if we have an accepted ipc stream. + if (IsCompletedSuccessfully(ipcClientStreamTask)) + ipcClientStreamTask.Result?.Dispose(); + + if (checkTcpStreamTask.IsFaulted) + { + _logger?.LogInformation("Broken tcp connection detected, aborting ipc connection."); + checkTcpStreamTask.GetAwaiter().GetResult(); + } + + throw; + } + + ipcClientStream = ipcClientStreamTask.Result; + + try + { + await IpcAdvertise.SerializeAsync(ipcClientStream, _runtimeInstanceId, _runtimeProcessId, token).ConfigureAwait(false); + } + catch (Exception) + { + _logger?.LogDebug("Failed sending advertise message."); + throw; + } + } + catch (Exception) + { + _logger?.LogDebug("Failed creating new router instance."); + + // Cleanup and rethrow. + tcpClientStream?.Dispose(); + ipcClientStream?.Dispose(); + + throw; + } + + // Create new router. + _logger?.LogDebug("New router instance successfully created."); + + return new Router(ipcClientStream, tcpClientStream, _logger, (ulong)IpcAdvertise.V1SizeInBytes); + } + } + internal class Router : IDisposable { readonly ILogger _logger; diff --git a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs index c4fdd81b2c..24546fe2c1 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs +++ b/src/Microsoft.Diagnostics.NETCore.Client/DiagnosticsServerRouter/DiagnosticsServerRouterRunner.cs @@ -22,19 +22,24 @@ internal interface Callbacks void OnRouterStopped(); } - public static async Task runIpcClientTcpServerRouter(CancellationToken token, string ipcClient, string tcpServer, int runtimeTimeoutMs, ILogger logger, Callbacks callbacks) + public static async Task runIpcClientTcpServerRouter(CancellationToken token, string ipcClient, string tcpServer, int runtimeTimeoutMs, TcpServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory, ILogger logger, Callbacks callbacks) { - return await runRouter(token, new IpcClientTcpServerRouterFactory(ipcClient, tcpServer, runtimeTimeoutMs, logger), callbacks).ConfigureAwait(false); + return await runRouter(token, new IpcClientTcpServerRouterFactory(ipcClient, tcpServer, runtimeTimeoutMs, tcpServerRouterFactory, logger), callbacks).ConfigureAwait(false); } - public static async Task runIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeoutMs, ILogger logger, Callbacks callbacks) + public static async Task runIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeoutMs, TcpServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory, ILogger logger, Callbacks callbacks) { - return await runRouter(token, new IpcServerTcpServerRouterFactory(ipcServer, tcpServer, runtimeTimeoutMs, logger), callbacks).ConfigureAwait(false); + return await runRouter(token, new IpcServerTcpServerRouterFactory(ipcServer, tcpServer, runtimeTimeoutMs, tcpServerRouterFactory, logger), callbacks).ConfigureAwait(false); } - public static async Task runIpcServerTcpClientRouter(CancellationToken token, string ipcServer, string tcpClient, int runtimeTimeoutMs, ILogger logger, Callbacks callbacks) + public static async Task runIpcServerTcpClientRouter(CancellationToken token, string ipcServer, string tcpClient, int runtimeTimeoutMs, TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory, ILogger logger, Callbacks callbacks) { - return await runRouter(token, new IpcServerTcpClientRouterFactory(ipcServer, tcpClient, runtimeTimeoutMs, logger), callbacks).ConfigureAwait(false); + return await runRouter(token, new IpcServerTcpClientRouterFactory(ipcServer, tcpClient, runtimeTimeoutMs, tcpClientRouterFactory, logger), callbacks).ConfigureAwait(false); + } + + public static async Task runIpcClientTcpClientRouter(CancellationToken token, string ipcClient, string tcpClient, int runtimeTimeoutMs, TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory, ILogger logger, Callbacks callbacks) + { + return await runRouter(token, new IpcClientTcpClientRouterFactory(ipcClient, tcpClient, runtimeTimeoutMs, tcpClientRouterFactory, logger), callbacks).ConfigureAwait(false); } public static bool isLoopbackOnly(string address) @@ -58,8 +63,9 @@ async static Task runRouter(CancellationToken token, DiagnosticsServerRoute try { - routerFactory.Start(); - callbacks?.OnRouterStarted(routerFactory.TcpAddress); + await routerFactory.Start(token); + if (!token.IsCancellationRequested) + callbacks?.OnRouterStarted(routerFactory.TcpAddress); while (!token.IsCancellationRequested) { diff --git a/src/Microsoft.Diagnostics.NETCore.Client/IpcHelpers.cs b/src/Microsoft.Diagnostics.NETCore.Client/IpcHelpers.cs new file mode 100644 index 0000000000..08ddc7eafa --- /dev/null +++ b/src/Microsoft.Diagnostics.NETCore.Client/IpcHelpers.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + internal static class IpcHelpers + { + public static string ReadString(byte[] buffer, ref int index) + { + // Length of the string of UTF-16 characters + int length = (int)BitConverter.ToUInt32(buffer, index); + index += sizeof(UInt32); + + int size = (int)length * sizeof(char); + // The string contains an ending null character; remove it before returning the value + string value = Encoding.Unicode.GetString(buffer, index, size).Substring(0, length - 1); + index += size; + return value; + } + } +} diff --git a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj index e53d0b81d0..ffba4a8aa9 100644 --- a/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj +++ b/src/Microsoft.Diagnostics.NETCore.Client/Microsoft.Diagnostics.NETCore.Client.csproj @@ -25,6 +25,7 @@ + diff --git a/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs b/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs index ef9109400b..f3bc44177d 100644 --- a/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs +++ b/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs @@ -64,7 +64,7 @@ public ModuleFromDebuggerServices( public override uint? IndexTimeStamp { get; } - public override VersionInfo? Version + public override VersionData VersionData { get { @@ -77,7 +77,7 @@ public override VersionInfo? Version int minor = (int)fileInfo.dwFileVersionMS & 0xffff; int revision = (int)fileInfo.dwFileVersionLS >> 16; int patch = (int)fileInfo.dwFileVersionLS & 0xffff; - base.Version = new VersionInfo(major, minor, revision, patch); + base.VersionData = new VersionData(major, minor, revision, patch); } else { @@ -87,7 +87,7 @@ public override VersionInfo? Version } } } - return base.Version; + return base.VersionData; } } diff --git a/src/SOS/SOS.Hosting/CorDebugDataTargetWrapper.cs b/src/SOS/SOS.Hosting/CorDebugDataTargetWrapper.cs index 5a4c3507dd..9139b399c3 100644 --- a/src/SOS/SOS.Hosting/CorDebugDataTargetWrapper.cs +++ b/src/SOS/SOS.Hosting/CorDebugDataTargetWrapper.cs @@ -55,6 +55,8 @@ internal CorDebugDataTargetWrapper(IServiceProvider services) builder = AddInterface(IID_ICorDebugMetaDataLocator, validate: false); builder.AddMethod(new GetMetaDataDelegate(GetMetaData)); builder.Complete(); + + AddRef(); } protected override void Destroy() diff --git a/src/SOS/SOS.Hosting/DataTargetWrapper.cs b/src/SOS/SOS.Hosting/DataTargetWrapper.cs index 57ec91d4fd..e8106b67e8 100644 --- a/src/SOS/SOS.Hosting/DataTargetWrapper.cs +++ b/src/SOS/SOS.Hosting/DataTargetWrapper.cs @@ -68,6 +68,8 @@ public DataTargetWrapper(IServiceProvider services, IRuntime runtime) builder = AddInterface(IID_ICLRRuntimeLocator, false); builder.AddMethod(new GetRuntimeBaseDelegate(GetRuntimeBase)); builder.Complete(); + + AddRef(); } void AddDataTarget(VTableBuilder builder) diff --git a/src/SOS/SOS.Hosting/RuntimeWrapper.cs b/src/SOS/SOS.Hosting/RuntimeWrapper.cs index 87f9b31d2e..79b91aa738 100644 --- a/src/SOS/SOS.Hosting/RuntimeWrapper.cs +++ b/src/SOS/SOS.Hosting/RuntimeWrapper.cs @@ -252,7 +252,7 @@ private int GetEEVersion( { return HResult.E_FAIL; } - if (!module.Version.HasValue) + if (module.VersionData is null) { return HResult.E_FAIL; } @@ -261,9 +261,9 @@ private int GetEEVersion( pFileInfo->dwFileFlagsMask = 0; pFileInfo->dwFileFlags = 0; - VersionInfo versionInfo = module.Version.Value; - pFileInfo->dwFileVersionMS = (uint)versionInfo.Minor | (uint)versionInfo.Major << 16; - pFileInfo->dwFileVersionLS = (uint)versionInfo.Patch | (uint)versionInfo.Revision << 16; + VersionData versionData = module.VersionData; + pFileInfo->dwFileVersionMS = (uint)versionData.Minor | (uint)versionData.Major << 16; + pFileInfo->dwFileVersionLS = (uint)versionData.Patch | (uint)versionData.Revision << 16; // Attempt to get the FileVersion string that contains version and the "built by" and commit id info if (fileVersionBuffer != null) @@ -306,13 +306,20 @@ private IntPtr CreateClrDataProcess() return IntPtr.Zero; } var dataTarget = new DataTargetWrapper(_services, _runtime); - int hr = createInstance(IID_IXCLRDataProcess, dataTarget.IDataTarget, out IntPtr unk); - if (hr != 0) + try { - Trace.TraceError($"CLRDataCreateInstance FAILED {hr:X8}"); - return IntPtr.Zero; + int hr = createInstance(IID_IXCLRDataProcess, dataTarget.IDataTarget, out IntPtr unk); + if (hr != 0) + { + Trace.TraceError($"CLRDataCreateInstance FAILED {hr:X8}"); + return IntPtr.Zero; + } + return unk; + } + finally + { + dataTarget.Release(); } - return unk; } private IntPtr CreateCorDebugProcess() @@ -343,96 +350,101 @@ private IntPtr CreateCorDebugProcess() var dataTarget = new CorDebugDataTargetWrapper(_services); ulong clrInstanceId = _runtime.RuntimeModule.ImageBase; int hresult = 0; - - var openVirtualProcessImpl2 = SOSHost.GetDelegateFunction(_dbiHandle, "OpenVirtualProcessImpl2"); - if (openVirtualProcessImpl2 != null) + try { - hresult = openVirtualProcessImpl2( - clrInstanceId, - dataTarget.ICorDebugDataTarget, - dacFilePath, - ref maxDebuggerSupportedVersion, - ref IID_ICorDebugProcess, - out IntPtr corDebugProcess, - out ClrDebuggingProcessFlags flags); - - if (hresult != 0) + var openVirtualProcessImpl2 = SOSHost.GetDelegateFunction(_dbiHandle, "OpenVirtualProcessImpl2"); + if (openVirtualProcessImpl2 != null) { - Trace.TraceError($"DBI OpenVirtualProcessImpl2 FAILED 0x{hresult:X8}"); - return IntPtr.Zero; + hresult = openVirtualProcessImpl2( + clrInstanceId, + dataTarget.ICorDebugDataTarget, + dacFilePath, + ref maxDebuggerSupportedVersion, + ref IID_ICorDebugProcess, + out IntPtr corDebugProcess, + out ClrDebuggingProcessFlags flags); + + if (hresult != 0) + { + Trace.TraceError($"DBI OpenVirtualProcessImpl2 FAILED 0x{hresult:X8}"); + return IntPtr.Zero; + } + Trace.TraceInformation($"DBI OpenVirtualProcessImpl2 SUCCEEDED"); + return corDebugProcess; } - Trace.TraceInformation($"DBI OpenVirtualProcessImpl2 SUCCEEDED"); - return corDebugProcess; - } - - IntPtr dacHandle = GetDacHandle(); - if (dacHandle == IntPtr.Zero) - { - return IntPtr.Zero; - } - // On Linux/MacOS the DAC module handle needs to be re-created using the DAC PAL instance - // before being passed to DBI's OpenVirtualProcess* implementation. The DBI and DAC share - // the same PAL where dbgshim has it's own. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - var loadLibraryFunction = SOSHost.GetDelegateFunction(dacHandle, "LoadLibraryW"); - if (loadLibraryFunction == null) + IntPtr dacHandle = GetDacHandle(); + if (dacHandle == IntPtr.Zero) { - Trace.TraceError($"Can not find the DAC LoadLibraryW export"); return IntPtr.Zero; } - dacHandle = loadLibraryFunction(dacFilePath); - if (dacHandle == IntPtr.Zero) + + // On Linux/MacOS the DAC module handle needs to be re-created using the DAC PAL instance + // before being passed to DBI's OpenVirtualProcess* implementation. The DBI and DAC share + // the same PAL where dbgshim has it's own. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - Trace.TraceError($"DAC LoadLibraryW({dacFilePath}) FAILED"); - return IntPtr.Zero; + var loadLibraryFunction = SOSHost.GetDelegateFunction(dacHandle, "LoadLibraryW"); + if (loadLibraryFunction == null) + { + Trace.TraceError($"Can not find the DAC LoadLibraryW export"); + return IntPtr.Zero; + } + dacHandle = loadLibraryFunction(dacFilePath); + if (dacHandle == IntPtr.Zero) + { + Trace.TraceError($"DAC LoadLibraryW({dacFilePath}) FAILED"); + return IntPtr.Zero; + } } - } - var openVirtualProcessImpl = SOSHost.GetDelegateFunction(_dbiHandle, "OpenVirtualProcessImpl"); - if (openVirtualProcessImpl != null) - { - hresult = openVirtualProcessImpl( - clrInstanceId, - dataTarget.ICorDebugDataTarget, - dacHandle, - ref maxDebuggerSupportedVersion, - ref IID_ICorDebugProcess, - out IntPtr corDebugProcess, - out ClrDebuggingProcessFlags flags); - - if (hresult != 0) + var openVirtualProcessImpl = SOSHost.GetDelegateFunction(_dbiHandle, "OpenVirtualProcessImpl"); + if (openVirtualProcessImpl != null) { - Trace.TraceError($"DBI OpenVirtualProcessImpl FAILED 0x{hresult:X8}"); - return IntPtr.Zero; + hresult = openVirtualProcessImpl( + clrInstanceId, + dataTarget.ICorDebugDataTarget, + dacHandle, + ref maxDebuggerSupportedVersion, + ref IID_ICorDebugProcess, + out IntPtr corDebugProcess, + out ClrDebuggingProcessFlags flags); + + if (hresult != 0) + { + Trace.TraceError($"DBI OpenVirtualProcessImpl FAILED 0x{hresult:X8}"); + return IntPtr.Zero; + } + Trace.TraceInformation($"DBI OpenVirtualProcessImpl SUCCEEDED"); + return corDebugProcess; } - Trace.TraceInformation($"DBI OpenVirtualProcessImpl SUCCEEDED"); - return corDebugProcess; - } - var openVirtualProcess = SOSHost.GetDelegateFunction(_dbiHandle, "OpenVirtualProcess"); - if (openVirtualProcess != null) - { - hresult = openVirtualProcess( - clrInstanceId, - dataTarget.ICorDebugDataTarget, - dacHandle, - ref IID_ICorDebugProcess, - out IntPtr corDebugProcess, - out ClrDebuggingProcessFlags flags); - - if (hresult != 0) + var openVirtualProcess = SOSHost.GetDelegateFunction(_dbiHandle, "OpenVirtualProcess"); + if (openVirtualProcess != null) { - Trace.TraceError($"DBI OpenVirtualProcess FAILED 0x{hresult:X8}"); - return IntPtr.Zero; + hresult = openVirtualProcess( + clrInstanceId, + dataTarget.ICorDebugDataTarget, + dacHandle, + ref IID_ICorDebugProcess, + out IntPtr corDebugProcess, + out ClrDebuggingProcessFlags flags); + + if (hresult != 0) + { + Trace.TraceError($"DBI OpenVirtualProcess FAILED 0x{hresult:X8}"); + return IntPtr.Zero; + } + Trace.TraceInformation($"DBI OpenVirtualProcess SUCCEEDED"); + return corDebugProcess; } - Trace.TraceInformation($"DBI OpenVirtualProcess SUCCEEDED"); - return corDebugProcess; + Trace.TraceError("DBI OpenVirtualProcess not found"); + return IntPtr.Zero; + } + finally + { + dataTarget.Release(); } - - Trace.TraceError("DBI OpenVirtualProcess not found"); - return IntPtr.Zero; } private IntPtr GetDacHandle() diff --git a/src/SOS/SOS.Hosting/SOSHost.cs b/src/SOS/SOS.Hosting/SOSHost.cs index 5189d58589..be78691cdb 100644 --- a/src/SOS/SOS.Hosting/SOSHost.cs +++ b/src/SOS/SOS.Hosting/SOSHost.cs @@ -479,7 +479,7 @@ internal unsafe int GetModuleVersionInformation( { return HResult.E_INVALIDARG; } - if (!module.Version.HasValue) + if (module.VersionData is null) { return HResult.E_FAIL; } @@ -489,9 +489,9 @@ internal unsafe int GetModuleVersionInformation( fileInfo->dwFileFlagsMask = 0; fileInfo->dwFileFlags = 0; - VersionInfo versionInfo = module.Version.Value; - fileInfo->dwFileVersionMS = (uint)versionInfo.Minor | (uint)versionInfo.Major << 16; - fileInfo->dwFileVersionLS = (uint)versionInfo.Patch | (uint)versionInfo.Revision << 16; + VersionData versionData = module.VersionData; + fileInfo->dwFileVersionMS = (uint)versionData.Minor | (uint)versionData.Major << 16; + fileInfo->dwFileVersionLS = (uint)versionData.Patch | (uint)versionData.Revision << 16; } else if (item == "\\StringFileInfo\\040904B0\\FileVersion") { diff --git a/src/SOS/Strike/strike.cpp b/src/SOS/Strike/strike.cpp index 203b32dc5e..3d74a7aadf 100644 --- a/src/SOS/Strike/strike.cpp +++ b/src/SOS/Strike/strike.cpp @@ -1570,6 +1570,27 @@ HRESULT PrintObj(TADDR taObj, BOOL bPrintFields = TRUE) } } + // Check for Tracked Type and tagged memory + ReleaseHolder sos11; + if (SUCCEEDED(g_sos->QueryInterface(__uuidof(ISOSDacInterface11), &sos11))) + { + CLRDATA_ADDRESS objAddr = TO_CDADDR(taObj); + BOOL isTrackedType; + BOOL hasTaggedMemory; + if (SUCCEEDED(sos11->IsTrackedType(objAddr, &isTrackedType, &hasTaggedMemory))) + { + ExtOut("Tracked Type: %s\n", isTrackedType ? "true" : "false"); + if (hasTaggedMemory) + { + CLRDATA_ADDRESS taggedMemory = NULL; + size_t taggedMemorySizeInBytes = 0; + (void)sos11->GetTaggedMemory(objAddr, &taggedMemory, &taggedMemorySizeInBytes); + DMLOut("Tagged Memory: %s (%" POINTERSIZE_TYPE "d(0x%" POINTERSIZE_TYPE "x) bytes)\n", + DMLTaggedMemory(taggedMemory, taggedMemorySizeInBytes / sizeof(void*)), taggedMemorySizeInBytes, taggedMemorySizeInBytes); + } + } + } + DWORD_PTR size = (DWORD_PTR)objData.Size; ExtOut("Size: %" POINTERSIZE_TYPE "d(0x%" POINTERSIZE_TYPE "x) bytes\n", size, size); @@ -6169,13 +6190,40 @@ enum { // These are the values set in m_dwTransientFlags. // Note that none of these flags survive a prejit save/restore. - M_CRST_NOTINITIALIZED = 0x00000001, // Used to prevent destruction of garbage m_crst - M_LOOKUPCRST_NOTINITIALIZED = 0x00000002, + MODULE_IS_TENURED = 0x00000001, // Set once we know for sure the Module will not be freed until the appdomain itself exits + // unused = 0x00000002, + CLASSES_FREED = 0x00000004, + IS_EDIT_AND_CONTINUE = 0x00000008, // is EnC Enabled for this module + + IS_PROFILER_NOTIFIED = 0x00000010, + IS_ETW_NOTIFIED = 0x00000020, - SUPPORTS_UPDATEABLE_METHODS = 0x00000020, - CLASSES_FREED = 0x00000040, - HAS_PHONY_IL_RVAS = 0x00000080, - IS_EDIT_AND_CONTINUE = 0x00000200, + // + // Note: the order of these must match the order defined in + // cordbpriv.h for DebuggerAssemblyControlFlags. The three + // values below should match the values defined in + // DebuggerAssemblyControlFlags when shifted right + // DEBUGGER_INFO_SHIFT bits. + // + DEBUGGER_USER_OVERRIDE_PRIV = 0x00000400, + DEBUGGER_ALLOW_JIT_OPTS_PRIV= 0x00000800, + DEBUGGER_TRACK_JIT_INFO_PRIV= 0x00001000, + DEBUGGER_ENC_ENABLED_PRIV = 0x00002000, // this is what was attempted to be set. IS_EDIT_AND_CONTINUE is actual result. + DEBUGGER_PDBS_COPIED = 0x00004000, + DEBUGGER_IGNORE_PDBS = 0x00008000, + DEBUGGER_INFO_MASK_PRIV = 0x0000Fc00, + DEBUGGER_INFO_SHIFT_PRIV = 10, + + // Used to indicate that this module has had it's IJW fixups properly installed. + IS_IJW_FIXED_UP = 0x00080000, + IS_BEING_UNLOADED = 0x00100000, + + // Used to indicate that the module is loaded sufficiently for generic candidate instantiations to work + MODULE_READY_FOR_TYPELOAD = 0x00200000, + + // Used during NGen only + TYPESPECS_TRIAGED = 0x40000000, + MODULE_SAVED = 0x80000000, }; void ModuleMapTraverse(UINT index, CLRDATA_ADDRESS methodTable, LPVOID token) @@ -6245,8 +6293,6 @@ DECLARE_API(DumpModule) ExtOut("PEFile "); if (module.bIsReflection) ExtOut("Reflection "); - if (module.dwTransientFlags & SUPPORTS_UPDATEABLE_METHODS) - ExtOut("SupportsUpdateableMethods "); ToRelease dataModule; if (SUCCEEDED(g_sos->GetModule(TO_CDADDR(p_ModuleAddr), &dataModule))) @@ -6264,6 +6310,11 @@ DECLARE_API(DumpModule) } ExtOut("\n"); + ExtOut("TransientFlags: %08x ", module.dwTransientFlags); + if (module.dwTransientFlags & IS_EDIT_AND_CONTINUE) + ExtOut("IS_EDIT_AND_CONTINUE"); + ExtOut("\n"); + DMLOut("Assembly: %s\n", DMLAssembly(module.Assembly)); ExtOut("BaseAddress: %p\n", SOS_PTR(module.ilBase)); @@ -8626,6 +8677,7 @@ DECLARE_API(ThreadPool) do // while (false) { UINT64 ui64Value = 0; + UINT32 ui32Value = 0; // Determine if the portable thread pool is enabled if (FAILED( @@ -8722,12 +8774,12 @@ DECLARE_API(ThreadPool) accumulatedOffset += offset; offset = GetValueFieldOffset(vCountsField.MTOfType, W("_data")); - if (offset < 0 || FAILED(MOVE(ui64Value, cdaTpInstance + accumulatedOffset + offset))) + if (offset < 0 || FAILED(MOVE(ui32Value, cdaTpInstance + accumulatedOffset + offset))) { ExtOut(" %s\n", "Failed to read PortableThreadPool._separated.counts._data"); break; } - UINT64 data = ui64Value; + UINT32 data = ui32Value; const UINT8 NumProcessingWorkShift = 0; const UINT8 NumExistingThreadsShift = 16; diff --git a/src/SOS/Strike/util.cpp b/src/SOS/Strike/util.cpp index e96899872f..6123b3b02b 100644 --- a/src/SOS/Strike/util.cpp +++ b/src/SOS/Strike/util.cpp @@ -4741,6 +4741,7 @@ const char * const DMLFormats[] = "%s", // DML_IL "%s", // DML_ComWrapperRCW "%s", // DML_ComWrapperCCW + "%s", // DML_TaggedMemory }; void ConvertToLower(__out_ecount(len) char *buffer, size_t len) @@ -4783,6 +4784,29 @@ CachedString Output::BuildHexValue(CLRDATA_ADDRESS addr, FormatType type, bool f return ret; } +CachedString Output::BuildHexValueWithLength(CLRDATA_ADDRESS addr, size_t len, FormatType type, bool fill) +{ + CachedString ret; + if (ret.IsOOM()) + { + ReportOOM(); + return ret; + } + + if (IsDMLEnabled()) + { + char hex[POINTERSIZE_BYTES*2 + 1]; + GetHex(addr, hex, _countof(hex), fill); + sprintf_s(ret, ret.GetStrLen(), DMLFormats[type], hex, len, hex); + } + else + { + GetHex(addr, ret, ret.GetStrLen(), fill); + } + + return ret; +} + CachedString Output::BuildVCValue(CLRDATA_ADDRESS mt, CLRDATA_ADDRESS addr, FormatType type, bool fill) { _ASSERTE(type == DML_ValueClass); diff --git a/src/SOS/Strike/util.h b/src/SOS/Strike/util.h index 638d50d0bc..51c3da0c64 100644 --- a/src/SOS/Strike/util.h +++ b/src/SOS/Strike/util.h @@ -484,7 +484,7 @@ class GCHeapDetails lowest_address = dacGCDetails.lowest_address; highest_address = dacGCDetails.highest_address; card_table = dacGCDetails.card_table; - has_regions = saved_sweep_ephemeral_seg == -1; + has_regions = generation_table[0].start_segment != generation_table[1].start_segment; } DacpGcHeapDetails original_heap_details; @@ -564,7 +564,8 @@ namespace Output DML_Async, DML_IL, DML_ComWrapperRCW, - DML_ComWrapperCCW + DML_ComWrapperCCW, + DML_TaggedMemory }; /**********************************************************************\ @@ -597,6 +598,21 @@ namespace Output \**********************************************************************/ CachedString BuildHexValue(CLRDATA_ADDRESS addr, FormatType type, bool fill = true); + /**********************************************************************\ + * This function builds a DML string for an object. If DML is enabled, * + * this function returns a DML string based on the format type. * + * Otherwise this returns a string containing only the hex value of * + * addr. * + * * + * Params: * + * addr - the address of the object * + * len - associated length * + * type - the format type to use to output this object * + * fill - whether or not to pad the hex value with zeros * + * * + \**********************************************************************/ + CachedString BuildHexValueWithLength(CLRDATA_ADDRESS addr, size_t len, FormatType type, bool fill = true); + /**********************************************************************\ * This function builds a DML string for an managed variable name. * * If DML is enabled, this function returns a DML string that will * @@ -672,6 +688,7 @@ inline void ExtOutIndent() { WhitespaceOut(Output::g_Indent << 2); } #define DMLIL(addr) Output::BuildHexValue(addr, Output::DML_IL).GetPtr() #define DMLComWrapperRCW(addr) Output::BuildHexValue(addr, Output::DML_ComWrapperRCW).GetPtr() #define DMLComWrapperCCW(addr) Output::BuildHexValue(addr, Output::DML_ComWrapperCCW).GetPtr() +#define DMLTaggedMemory(addr, len) Output::BuildHexValueWithLength(addr, len, Output::DML_TaggedMemory).GetPtr() bool IsDMLEnabled(); diff --git a/src/SOS/dbgutil/elfreader.cpp b/src/SOS/dbgutil/elfreader.cpp index a8d00fa311..0deebf9456 100644 --- a/src/SOS/dbgutil/elfreader.cpp +++ b/src/SOS/dbgutil/elfreader.cpp @@ -393,10 +393,11 @@ ElfReader::EnumerateLinkMapEntries(Elf_Dyn* dynamicAddr) for (; i < PATH_MAX; i++) { char ch; - if (!ReadMemory(map.l_name + i, &ch, sizeof(ch))) { - TRACE("DSO: ReadMemory link_map name %p + %d FAILED\n", map.l_name, i); + char* l_name = const_cast(map.l_name); + if (!ReadMemory(l_name + i, &ch, sizeof(ch))) { + TRACE("DSO: ReadMemory link_map name %p + %d FAILED\n", map.l_name, i); break; - } + } if (ch == '\0') { break; } diff --git a/src/SOS/dbgutil/elfreader.h b/src/SOS/dbgutil/elfreader.h index 93cc57963f..fe1648db4e 100644 --- a/src/SOS/dbgutil/elfreader.h +++ b/src/SOS/dbgutil/elfreader.h @@ -63,7 +63,11 @@ class ElfReader bool EnumerateLinkMapEntries(ElfW(Dyn)* dynamicAddr); #endif bool EnumerateProgramHeaders(ElfW(Phdr)* phdrAddr, int phnum, uint64_t baseAddress, uint64_t* ploadbias, ElfW(Dyn)** pdynamicAddr); +#ifdef __FreeBSD__ + virtual void VisitModule(caddr_t baseAddress, std::string& moduleName) { }; +#else virtual void VisitModule(uint64_t baseAddress, std::string& moduleName) { }; +#endif virtual void VisitProgramHeader(uint64_t loadbias, uint64_t baseAddress, ElfW(Phdr)* phdr) { }; virtual bool ReadMemory(void* address, void* buffer, size_t size) = 0; virtual void Trace(const char* format, ...) { }; diff --git a/src/SOS/extensions/hostcoreclr.cpp b/src/SOS/extensions/hostcoreclr.cpp index b7d823bcc1..4d22a48bc0 100644 --- a/src/SOS/extensions/hostcoreclr.cpp +++ b/src/SOS/extensions/hostcoreclr.cpp @@ -22,6 +22,11 @@ #include #endif +#if defined(__FreeBSD__) +#include +#include +#endif + #include "palclr.h" #include "arrayholder.h" #include "coreclrhost.h" @@ -412,8 +417,8 @@ static HRESULT GetHostRuntime(std::string& coreClrPath, std::string& hostRuntime else { #ifdef FEATURE_PAL -#if defined (__FreeBSD__) || defined(__NetBSD__) - TraceError("Hosting on FreeBSD or NetBSD not supported\n"); +#if defined(__NetBSD__) + TraceError("Hosting on NetBSD not supported\n"); return E_FAIL; #else char* line = nullptr; @@ -446,7 +451,7 @@ static HRESULT GetHostRuntime(std::string& coreClrPath, std::string& hostRuntime } } } -#endif // defined (__FreeBSD__) || defined(__NetBSD__) +#endif // defined(__NetBSD__) #else ArrayHolder programFiles = new CHAR[MAX_LONGPATH]; if (GetEnvironmentVariableA("PROGRAMFILES", programFiles, MAX_LONGPATH) == 0) diff --git a/src/SOS/lldbplugin/CMakeLists.txt b/src/SOS/lldbplugin/CMakeLists.txt index b850a7886a..9865a6f8a5 100644 --- a/src/SOS/lldbplugin/CMakeLists.txt +++ b/src/SOS/lldbplugin/CMakeLists.txt @@ -114,6 +114,10 @@ else() #FreeBSD find_path(LLDB_H "lldb/API/LLDB.h" PATHS "/usr/local/llvm39/include") find_path(LLDB_H "lldb/API/LLDB.h" PATHS "/usr/local/llvm38/include") + find_path(LLDB_H "lldb/API/LLDB.h" PATHS "/usr/local/llvm12/include") + find_path(LLDB_H "lldb/API/LLDB.h" PATHS "/usr/local/llvm11/include") + find_path(LLDB_H "lldb/API/LLDB.h" PATHS "/usr/local/llvm10/include") + find_path(LLDB_H "lldb/API/LLDB.h" PATHS "/usr/local/llvm90/include") if(LLDB_H STREQUAL LLDB_H-NOTFOUND) if(REQUIRE_LLDBPLUGIN) diff --git a/src/Tools/Common/Commands/Utils.cs b/src/Tools/Common/Commands/Utils.cs index 2205e98758..cb501fb387 100644 --- a/src/Tools/Common/Commands/Utils.cs +++ b/src/Tools/Common/Commands/Utils.cs @@ -161,4 +161,13 @@ public void RewriteConsoleLine() private void SystemConsoleLineRewriter() => Console.SetCursorPosition(0, LineToClear); } + + internal class ReturnCode + { + public static int Ok = 0; + public static int SessionCreationError = 1; + public static int TracingError = 2; + public static int ArgumentError = 3; + public static int UnknownError = 4; + } } diff --git a/src/Tools/Common/ReversedServerHelpers/ReversedServerHelpers.cs b/src/Tools/Common/ReversedServerHelpers/ReversedServerHelpers.cs index f98961183d..71bc1fb793 100644 --- a/src/Tools/Common/ReversedServerHelpers/ReversedServerHelpers.cs +++ b/src/Tools/Common/ReversedServerHelpers/ReversedServerHelpers.cs @@ -190,8 +190,15 @@ public DiagnosticsClientBuilder(string toolName, int timeoutInSec) _timeoutInSec = timeoutInSec; } - public async Task Build(CancellationToken ct, int processId, string portName, bool showChildIO, bool printLaunchCommand) + public async Task Build(CancellationToken ct, int processId, string diagnosticPort, bool showChildIO, bool printLaunchCommand) { + IpcEndpointConfig portConfig = null; + + if (!string.IsNullOrEmpty(diagnosticPort)) + { + portConfig = IpcEndpointConfig.Parse(diagnosticPort); + } + if (ProcessLauncher.Launcher.HasChildProc) { // Create and start the reversed server @@ -223,9 +230,9 @@ public async Task Build(CancellationToken ct, int proce } return new DiagnosticsClientHolder(new DiagnosticsClient(endpointInfo.Endpoint), endpointInfo, server); } - else if (!string.IsNullOrEmpty(portName)) + else if (portConfig != null && portConfig.IsListenConfig) { - string fullPort = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? portName : Path.GetFullPath(portName); + string fullPort = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? portConfig.Address : Path.GetFullPath(portConfig.Address); ReversedDiagnosticsServer server = new ReversedDiagnosticsServer(fullPort); server.Start(); Console.WriteLine($"Waiting for connection on {fullPort}"); @@ -245,6 +252,10 @@ public async Task Build(CancellationToken ct, int proce return null; } } + else if (portConfig != null && portConfig.IsConnectConfig) + { + return new DiagnosticsClientHolder(new DiagnosticsClient(portConfig)); + } else { return new DiagnosticsClientHolder(new DiagnosticsClient(processId)); diff --git a/src/Tools/dotnet-counters/CounterMonitor.cs b/src/Tools/dotnet-counters/CounterMonitor.cs index 185259014c..9f1ba309fc 100644 --- a/src/Tools/dotnet-counters/CounterMonitor.cs +++ b/src/Tools/dotnet-counters/CounterMonitor.cs @@ -32,7 +32,7 @@ public class CounterMonitor private string _output; private bool pauseCmdSet; private ManualResetEvent shouldExit; - private bool shouldResumeRuntime; + private bool _resumeRuntime; private DiagnosticsClient _diagnosticsClient; private EventPipeSession _session; @@ -91,11 +91,11 @@ private void StopMonitor() _renderer.Stop(); } - public async Task Monitor(CancellationToken ct, List counter_list, string counters, IConsole console, int processId, int refreshInterval, string name, string diagnosticPort) + public async Task Monitor(CancellationToken ct, List counter_list, string counters, IConsole console, int processId, int refreshInterval, string name, string diagnosticPort, bool resumeRuntime) { if (!ProcessLauncher.Launcher.HasChildProc && !CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out _processId)) { - return 0; + return ReturnCode.ArgumentError; } shouldExit = new ManualResetEvent(false); _ct.Register(() => shouldExit.Set()); @@ -105,7 +105,7 @@ public async Task Monitor(CancellationToken ct, List counter_list, { if (holder == null) { - return 1; + return ReturnCode.Ok; } try { @@ -115,7 +115,7 @@ public async Task Monitor(CancellationToken ct, List counter_list, _interval = refreshInterval; _renderer = new ConsoleWriter(); _diagnosticsClient = holder.Client; - shouldResumeRuntime = ProcessLauncher.Launcher.HasChildProc || !string.IsNullOrEmpty(diagnosticPort); + _resumeRuntime = resumeRuntime; int ret = await Start(); ProcessLauncher.Launcher.Cleanup(); return ret; @@ -129,17 +129,17 @@ public async Task Monitor(CancellationToken ct, List counter_list, catch (Exception) { } // Swallow all exceptions for now. console.Out.WriteLine($"Complete"); - return 1; + return ReturnCode.Ok; } } } - public async Task Collect(CancellationToken ct, List counter_list, string counters, IConsole console, int processId, int refreshInterval, CountersExportFormat format, string output, string name, string diagnosticPort) + public async Task Collect(CancellationToken ct, List counter_list, string counters, IConsole console, int processId, int refreshInterval, CountersExportFormat format, string output, string name, string diagnosticPort, bool resumeRuntime) { if (!ProcessLauncher.Launcher.HasChildProc && !CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out _processId)) { - return 0; + return ReturnCode.ArgumentError; } shouldExit = new ManualResetEvent(false); @@ -150,7 +150,7 @@ public async Task Collect(CancellationToken ct, List counter_list, { if (holder == null) { - return 1; + return ReturnCode.Ok; } try @@ -164,7 +164,7 @@ public async Task Collect(CancellationToken ct, List counter_list, if (_output.Length == 0) { _console.Error.WriteLine("Output cannot be an empty string"); - return 0; + return ReturnCode.ArgumentError; } if (format == CountersExportFormat.csv) { @@ -188,9 +188,9 @@ public async Task Collect(CancellationToken ct, List counter_list, else { _console.Error.WriteLine($"The output format {format} is not a valid output format."); - return 0; + return ReturnCode.ArgumentError; } - shouldResumeRuntime = ProcessLauncher.Launcher.HasChildProc || !string.IsNullOrEmpty(diagnosticPort); + _resumeRuntime = resumeRuntime; int ret = await Start(); return ret; } @@ -201,7 +201,7 @@ public async Task Collect(CancellationToken ct, List counter_list, _session.Stop(); } catch (Exception) { } // session.Stop() can throw if target application already stopped before we send the stop command. - return 1; + return ReturnCode.Ok; } } } @@ -332,7 +332,7 @@ private async Task Start() string providerString = BuildProviderString(); if (providerString.Length == 0) { - return 1; + return ReturnCode.ArgumentError; } _renderer.Initialize(); @@ -341,9 +341,16 @@ private async Task Start() try { _session = _diagnosticsClient.StartEventPipeSession(Trace.Extensions.ToProviders(providerString), false, 10); - if (shouldResumeRuntime) + if (_resumeRuntime) { - _diagnosticsClient.ResumeRuntime(); + try + { + _diagnosticsClient.ResumeRuntime(); + } + catch (UnsupportedCommandException) + { + // Noop if the command is unknown since the target process is most likely a 3.1 app. + } } var source = new EventPipeEventSource(_session.EventStream); source.Dynamic.All += DynamicAllMonitor; @@ -373,7 +380,7 @@ private async Task Start() if (shouldExit.WaitOne(250)) { StopMonitor(); - return 0; + return ReturnCode.Ok; } if (Console.KeyAvailable) { @@ -395,7 +402,7 @@ private async Task Start() pauseCmdSet = false; } } - return await Task.FromResult(0); + return await Task.FromResult(ReturnCode.Ok); } } } diff --git a/src/Tools/dotnet-counters/Program.cs b/src/Tools/dotnet-counters/Program.cs index 5da067fcd9..b321060313 100644 --- a/src/Tools/dotnet-counters/Program.cs +++ b/src/Tools/dotnet-counters/Program.cs @@ -22,8 +22,8 @@ public enum CountersExportFormat { csv, json }; internal class Program { - delegate Task CollectDelegate(CancellationToken ct, List counter_list, string counters, IConsole console, int processId, int refreshInterval, CountersExportFormat format, string output, string processName, string port); - delegate Task MonitorDelegate(CancellationToken ct, List counter_list, string counters, IConsole console, int processId, int refreshInterval, string processName, string port); + delegate Task CollectDelegate(CancellationToken ct, List counter_list, string counters, IConsole console, int processId, int refreshInterval, CountersExportFormat format, string output, string processName, string port, bool resumeRuntime); + delegate Task MonitorDelegate(CancellationToken ct, List counter_list, string counters, IConsole console, int processId, int refreshInterval, string processName, string port, bool resumeRuntime); private static Command MonitorCommand() => new Command( @@ -33,7 +33,7 @@ private static Command MonitorCommand() => // Handler HandlerDescriptor.FromDelegate((MonitorDelegate)new CounterMonitor().Monitor).GetCommandHandler(), // Arguments and Options - CounterList(), CounterOption(), ProcessIdOption(), RefreshIntervalOption(), NameOption(), DiagnosticPortOption(), + CounterList(), CounterOption(), ProcessIdOption(), RefreshIntervalOption(), NameOption(), DiagnosticPortOption(), ResumeRuntimeOption() }; private static Command CollectCommand() => @@ -44,7 +44,7 @@ private static Command CollectCommand() => // Handler HandlerDescriptor.FromDelegate((CollectDelegate)new CounterMonitor().Collect).GetCommandHandler(), // Arguments and Options - CounterList(), CounterOption(), ProcessIdOption(), RefreshIntervalOption(), ExportFormatOption(), ExportFileNameOption(), NameOption(), DiagnosticPortOption() + CounterList(), CounterOption(), ProcessIdOption(), RefreshIntervalOption(), ExportFormatOption(), ExportFileNameOption(), NameOption(), DiagnosticPortOption(), ResumeRuntimeOption() }; private static Option NameOption() => @@ -122,11 +122,19 @@ private static Option RuntimeVersionOption() => private static Option DiagnosticPortOption() => new Option( alias: "--diagnostic-port", - description: "The path to diagnostic port") + description: "The path to diagnostic port to be used.") { Argument = new Argument(name: "diagnosticPort", getDefaultValue: () => "") }; + private static Option ResumeRuntimeOption() => + new Option( + alias: "--resume-runtime", + description: @"Resume runtime once session has been initialized, defaults to true. Disable resume of runtime using --resume-runtime:false") + { + Argument = new Argument(name: "resumeRuntime", getDefaultValue: () => true) + }; + private static readonly string[] s_SupportedRuntimeVersions = new[] { "3.0", "3.1", "5.0" }; public static int List(IConsole console, string runtimeVersion) diff --git a/src/Tools/dotnet-dsrouter/ADBTcpRouterFactory.cs b/src/Tools/dotnet-dsrouter/ADBTcpRouterFactory.cs new file mode 100644 index 0000000000..f74159309d --- /dev/null +++ b/src/Tools/dotnet-dsrouter/ADBTcpRouterFactory.cs @@ -0,0 +1,173 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter +{ + internal class ADBCommandExec + { + public static bool AdbAddPortForward(int port, ILogger logger) + { + bool ownsPortForward = false; + if (!RunAdbCommandInternal($"forward --list", $"tcp:{port}", 0, logger)) + { + ownsPortForward = RunAdbCommandInternal($"forward tcp:{port} tcp:{port}", "", 0, logger); + if (!ownsPortForward) + logger?.LogError($"Failed setting up port forward for tcp:{port}."); + } + return ownsPortForward; + } + + public static bool AdbAddPortReverse(int port, ILogger logger) + { + bool ownsPortForward = false; + if (!RunAdbCommandInternal($"reverse --list", $"tcp:{port}", 0, logger)) + { + ownsPortForward = RunAdbCommandInternal($"reverse tcp:{port} tcp:{port}", "", 0, logger); + if (!ownsPortForward) + logger?.LogError($"Failed setting up port forward for tcp:{port}."); + } + return ownsPortForward; + } + + public static void AdbRemovePortForward(int port, bool ownsPortForward, ILogger logger) + { + if (ownsPortForward) + { + if (!RunAdbCommandInternal($"forward --remove tcp:{port}", "", 0, logger)) + logger?.LogError($"Failed removing port forward for tcp:{port}."); + } + } + + public static void AdbRemovePortReverse(int port, bool ownsPortForward, ILogger logger) + { + if (ownsPortForward) + { + if (!RunAdbCommandInternal($"reverse --remove tcp:{port}", "", 0, logger)) + logger?.LogError($"Failed removing port forward for tcp:{port}."); + } + } + + public static bool RunAdbCommandInternal(string command, string expectedOutput, int expectedExitCode, ILogger logger) + { + var sdkRoot = Environment.GetEnvironmentVariable("ANDROID_SDK_ROOT"); + var adbTool = "adb"; + + if (!string.IsNullOrEmpty(sdkRoot)) + adbTool = sdkRoot + Path.DirectorySeparatorChar + "platform-tools" + Path.DirectorySeparatorChar + adbTool; + + logger?.LogDebug($"Executing {adbTool} {command}."); + + var process = new Process(); + process.StartInfo.FileName = adbTool; + process.StartInfo.Arguments = command; + + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.RedirectStandardInput = false; + + bool processStartedResult = false; + bool expectedOutputResult = true; + bool expectedExitCodeResult = true; + + try + { + processStartedResult = process.Start(); + } + catch (Exception) + { + } + + if (processStartedResult) + { + var stdout = process.StandardOutput.ReadToEnd(); + var stderr = process.StandardError.ReadToEnd(); + + if (!string.IsNullOrEmpty(expectedOutput)) + expectedOutputResult = !string.IsNullOrEmpty(stdout) ? stdout.Contains(expectedOutput) : false; + + if (!string.IsNullOrEmpty(stdout)) + logger.LogTrace($"stdout: {stdout}"); + + if (!string.IsNullOrEmpty(stderr)) + logger.LogError($"stderr: {stderr}"); + } + + if (processStartedResult) + { + process.WaitForExit(); + expectedExitCodeResult = (expectedExitCode != -1) ? (process.ExitCode == expectedExitCode) : true; + } + + return processStartedResult && expectedOutputResult && expectedExitCodeResult; + } + } + + internal class ADBTcpServerRouterFactory : TcpServerRouterFactory + { + readonly int _port; + bool _ownsPortReverse; + + public static TcpServerRouterFactory CreateADBInstance(string tcpServer, int runtimeTimeoutMs, ILogger logger) + { + return new ADBTcpServerRouterFactory(tcpServer, runtimeTimeoutMs, logger); + } + + public ADBTcpServerRouterFactory(string tcpServer, int runtimeTimeoutMs, ILogger logger) + : base(tcpServer, runtimeTimeoutMs, logger) + { + _port = new IpcTcpSocketEndPoint(tcpServer).EndPoint.Port; + } + + public override void Start() + { + // Enable port reverse. + _ownsPortReverse = ADBCommandExec.AdbAddPortReverse(_port, _logger); + + base.Start(); + } + + public override async Task Stop() + { + await base.Stop().ConfigureAwait(false); + + // Disable port reverse. + ADBCommandExec.AdbRemovePortReverse(_port, _ownsPortReverse, _logger); + _ownsPortReverse = false; + } + } + + internal class ADBTcpClientRouterFactory : TcpClientRouterFactory + { + readonly int _port; + bool _ownsPortForward; + + public static TcpClientRouterFactory CreateADBInstance(string tcpClient, int runtimeTimeoutMs, ILogger logger) + { + return new ADBTcpClientRouterFactory(tcpClient, runtimeTimeoutMs, logger); + } + + public ADBTcpClientRouterFactory(string tcpClient, int runtimeTimeoutMs, ILogger logger) + : base(tcpClient, runtimeTimeoutMs, logger) + { + _port = new IpcTcpSocketEndPoint(tcpClient).EndPoint.Port; + } + + public override void Start() + { + // Enable port forwarding. + _ownsPortForward = ADBCommandExec.AdbAddPortForward(_port, _logger); + } + + public override void Stop() + { + // Disable port forwarding. + ADBCommandExec.AdbRemovePortForward(_port, _ownsPortForward, _logger); + _ownsPortForward = false; + } + } +} diff --git a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs index c51639f3d8..85caeac333 100644 --- a/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs +++ b/src/Tools/dotnet-dsrouter/DiagnosticsServerRouterCommands.cs @@ -5,6 +5,9 @@ using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Internal.Common.Utils; using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -48,7 +51,7 @@ public DiagnosticsServerRouterCommands() { } - public async Task RunIpcClientTcpServerRouter(CancellationToken token, string ipcClient, string tcpServer, int runtimeTimeout, string verbose) + public async Task RunIpcClientTcpServerRouter(CancellationToken token, string ipcClient, string tcpServer, int runtimeTimeout, string verbose, string forwardPort) { checkLoopbackOnly(tcpServer); @@ -69,7 +72,22 @@ public async Task RunIpcClientTcpServerRouter(CancellationToken token, stri Launcher.Verbose = logLevel != LogLevel.Information; Launcher.CommandToken = token; - var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpServerRouter(linkedCancelToken.Token, ipcClient, tcpServer, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, factory.CreateLogger("dotnet-dsrounter"), Launcher); + var logger = factory.CreateLogger("dotnet-dsrouter"); + + TcpServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory = TcpServerRouterFactory.CreateDefaultInstance; + if (!string.IsNullOrEmpty(forwardPort)) + { + if (string.Compare(forwardPort, "android", StringComparison.OrdinalIgnoreCase) == 0) + { + tcpServerRouterFactory = ADBTcpServerRouterFactory.CreateADBInstance; + } + else + { + logger.LogError($"Unknown port forwarding argument, {forwardPort}. Only Android port fowarding is supported for TcpServer mode. Ignoring --forward-port argument."); + } + } + + var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpServerRouter(linkedCancelToken.Token, ipcClient, tcpServer, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpServerRouterFactory, logger, Launcher); while (!linkedCancelToken.IsCancellationRequested) { @@ -91,7 +109,7 @@ public async Task RunIpcClientTcpServerRouter(CancellationToken token, stri return routerTask.Result; } - public async Task RunIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeout, string verbose) + public async Task RunIpcServerTcpServerRouter(CancellationToken token, string ipcServer, string tcpServer, int runtimeTimeout, string verbose, string forwardPort) { checkLoopbackOnly(tcpServer); @@ -112,7 +130,25 @@ public async Task RunIpcServerTcpServerRouter(CancellationToken token, stri Launcher.Verbose = logLevel != LogLevel.Information; Launcher.CommandToken = token; - var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, tcpServer, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, factory.CreateLogger("dotnet-dsrounter"), Launcher); + var logger = factory.CreateLogger("dotnet-dsrouter"); + + TcpServerRouterFactory.CreateInstanceDelegate tcpServerRouterFactory = TcpServerRouterFactory.CreateDefaultInstance; + if (!string.IsNullOrEmpty(forwardPort)) + { + if (string.Compare(forwardPort, "android", StringComparison.OrdinalIgnoreCase) == 0) + { + tcpServerRouterFactory = ADBTcpServerRouterFactory.CreateADBInstance; + } + else + { + logger.LogError($"Unknown port forwarding argument, {forwardPort}. Only Android port fowarding is supported for TcpServer mode. Ignoring --forward-port argument."); + } + } + + if (string.IsNullOrEmpty(ipcServer)) + ipcServer = GetDefaultIpcServerPath(logger); + + var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpServerRouter(linkedCancelToken.Token, ipcServer, tcpServer, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpServerRouterFactory, logger, Launcher); while (!linkedCancelToken.IsCancellationRequested) { @@ -134,7 +170,7 @@ public async Task RunIpcServerTcpServerRouter(CancellationToken token, stri return routerTask.Result; } - public async Task RunIpcServerTcpClientRouter(CancellationToken token, string ipcServer, string tcpClient, int runtimeTimeout, string verbose) + public async Task RunIpcServerTcpClientRouter(CancellationToken token, string ipcServer, string tcpClient, int runtimeTimeout, string verbose, string forwardPort) { using CancellationTokenSource cancelRouterTask = new CancellationTokenSource(); using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token); @@ -153,7 +189,26 @@ public async Task RunIpcServerTcpClientRouter(CancellationToken token, stri Launcher.Verbose = logLevel != LogLevel.Information; Launcher.CommandToken = token; - var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpClientRouter(linkedCancelToken.Token, ipcServer, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, factory.CreateLogger("dotnet-dsrounter"), Launcher); + var logger = factory.CreateLogger("dotnet-dsrouter"); + + TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory = TcpClientRouterFactory.CreateDefaultInstance; + if (!string.IsNullOrEmpty(forwardPort)) + { + if (string.Compare(forwardPort, "android", StringComparison.OrdinalIgnoreCase) == 0) + { + tcpClientRouterFactory = ADBTcpClientRouterFactory.CreateADBInstance; + } + else if (string.Compare(forwardPort, "ios", StringComparison.OrdinalIgnoreCase) == 0) + { + tcpClientRouterFactory = USBMuxTcpClientRouterFactory.CreateUSBMuxInstance; + } + else + { + logger.LogError($"Unknown port forwarding argument, {forwardPort}. Ignoring --forward-port argument."); + } + } + + var routerTask = DiagnosticsServerRouterRunner.runIpcServerTcpClientRouter(linkedCancelToken.Token, ipcServer, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpClientRouterFactory, logger, Launcher); while (!linkedCancelToken.IsCancellationRequested) { @@ -175,6 +230,105 @@ public async Task RunIpcServerTcpClientRouter(CancellationToken token, stri return routerTask.Result; } + public async Task RunIpcClientTcpClientRouter(CancellationToken token, string ipcClient, string tcpClient, int runtimeTimeout, string verbose, string forwardPort) + { + using CancellationTokenSource cancelRouterTask = new CancellationTokenSource(); + using CancellationTokenSource linkedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(token, cancelRouterTask.Token); + + LogLevel logLevel = LogLevel.Information; + if (string.Compare(verbose, "debug", StringComparison.OrdinalIgnoreCase) == 0) + logLevel = LogLevel.Debug; + else if (string.Compare(verbose, "trace", StringComparison.OrdinalIgnoreCase) == 0) + logLevel = LogLevel.Trace; + + using var factory = new LoggerFactory(); + factory.AddConsole(logLevel, false); + + Launcher.SuspendProcess = true; + Launcher.ConnectMode = false; + Launcher.Verbose = logLevel != LogLevel.Information; + Launcher.CommandToken = token; + + var logger = factory.CreateLogger("dotnet-dsrouter"); + + TcpClientRouterFactory.CreateInstanceDelegate tcpClientRouterFactory = TcpClientRouterFactory.CreateDefaultInstance; + if (!string.IsNullOrEmpty(forwardPort)) + { + if (string.Compare(forwardPort, "android", StringComparison.OrdinalIgnoreCase) == 0) + { + tcpClientRouterFactory = ADBTcpClientRouterFactory.CreateADBInstance; + } + else if (string.Compare(forwardPort, "ios", StringComparison.OrdinalIgnoreCase) == 0) + { + tcpClientRouterFactory = USBMuxTcpClientRouterFactory.CreateUSBMuxInstance; + } + else + { + logger.LogError($"Unknown port forwarding argument, {forwardPort}. Ignoring --forward-port argument."); + } + } + + var routerTask = DiagnosticsServerRouterRunner.runIpcClientTcpClientRouter(linkedCancelToken.Token, ipcClient, tcpClient, runtimeTimeout == Timeout.Infinite ? runtimeTimeout : runtimeTimeout * 1000, tcpClientRouterFactory, logger, Launcher); + + while (!linkedCancelToken.IsCancellationRequested) + { + await Task.WhenAny(routerTask, Task.Delay(250)).ConfigureAwait(false); + if (routerTask.IsCompleted) + break; + + if (!Console.IsInputRedirected && Console.KeyAvailable) + { + ConsoleKey cmd = Console.ReadKey(true).Key; + if (cmd == ConsoleKey.Q) + { + cancelRouterTask.Cancel(); + break; + } + } + } + + return routerTask.Result; + } + + static string GetDefaultIpcServerPath(ILogger logger) + { + int processId = Process.GetCurrentProcess().Id; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}"); + if (File.Exists(path)) + { + logger?.LogWarning($"Default IPC server path, {path}, already in use. To disable default diagnostics for dotnet-dsrouter, set COMPlus_EnableDiagnostics=0 and re-run."); + + path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-dsrouter-{processId}"); + logger?.LogWarning($"Fallback using none default IPC server path, {path}."); + } + + return path.Substring(PidIpcEndpoint.IpcRootPath.Length); + } + else + { + DateTime unixEpoch; +#if NETCOREAPP2_1_OR_GREATER + unixEpoch = DateTime.UnixEpoch; +#else + unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); +#endif + TimeSpan diff = Process.GetCurrentProcess().StartTime.ToUniversalTime() - unixEpoch; + + var path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-{(long)diff.TotalSeconds}-socket"); + if (Directory.GetFiles(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-*-socket").Length != 0) + { + logger?.LogWarning($"Default IPC server path, {Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-diagnostic-{processId}-*-socket")}, already in use. To disable default diagnostics for dotnet-dsrouter, set COMPlus_EnableDiagnostics=0 and re-run."); + + path = Path.Combine(PidIpcEndpoint.IpcRootPath, $"dotnet-dsrouter-{processId}-{(long)diff.TotalSeconds}-socket"); + logger?.LogWarning($"Fallback using none default IPC server path, {path}."); + } + + return path; + } + } + static void checkLoopbackOnly(string tcpServer) { if (!string.IsNullOrEmpty(tcpServer) && !DiagnosticsServerRouterRunner.isLoopbackOnly(tcpServer)) diff --git a/src/Tools/dotnet-dsrouter/Program.cs b/src/Tools/dotnet-dsrouter/Program.cs index a56da4fbb8..7b9e08a8ed 100644 --- a/src/Tools/dotnet-dsrouter/Program.cs +++ b/src/Tools/dotnet-dsrouter/Program.cs @@ -17,9 +17,10 @@ namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter { internal class Program { - delegate Task DiagnosticsServerIpcClientTcpServerRouterDelegate(CancellationToken ct, string ipcClient, string tcpServer, int runtimeTimeoutS, string verbose); - delegate Task DiagnosticsServerIpcServerTcpServerRouterDelegate(CancellationToken ct, string ipcServer, string tcpServer, int runtimeTimeoutS, string verbose); - delegate Task DiagnosticsServerIpcServerTcpClientRouterDelegate(CancellationToken ct, string ipcServer, string tcpClient, int runtimeTimeoutS, string verbose); + delegate Task DiagnosticsServerIpcClientTcpServerRouterDelegate(CancellationToken ct, string ipcClient, string tcpServer, int runtimeTimeoutS, string verbose, string forwardPort); + delegate Task DiagnosticsServerIpcServerTcpServerRouterDelegate(CancellationToken ct, string ipcServer, string tcpServer, int runtimeTimeoutS, string verbose, string forwardPort); + delegate Task DiagnosticsServerIpcServerTcpClientRouterDelegate(CancellationToken ct, string ipcServer, string tcpClient, int runtimeTimeoutS, string verbose, string forwardPort); + delegate Task DiagnosticsServerIpcClientTcpClientRouterDelegate(CancellationToken ct, string ipcClient, string tcpClient, int runtimeTimeoutS, string verbose, string forwardPort); private static Command IpcClientTcpServerRouterCommand() => new Command( @@ -31,7 +32,7 @@ private static Command IpcClientTcpServerRouterCommand() => // Handler HandlerDescriptor.FromDelegate((DiagnosticsServerIpcClientTcpServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcClientTcpServerRouter).GetCommandHandler(), // Options - IpcClientAddressOption(), TcpServerAddressOption(), RuntimeTimeoutOption(), VerboseOption() + IpcClientAddressOption(), TcpServerAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption() }; private static Command IpcServerTcpServerRouterCommand() => @@ -44,7 +45,7 @@ private static Command IpcServerTcpServerRouterCommand() => // Handler HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerTcpServerRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerTcpServerRouter).GetCommandHandler(), // Options - IpcServerAddressOption(), TcpServerAddressOption(), RuntimeTimeoutOption(), VerboseOption() + IpcServerAddressOption(), TcpServerAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption() }; private static Command IpcServerTcpClientRouterCommand() => @@ -57,7 +58,20 @@ private static Command IpcServerTcpClientRouterCommand() => // Handler HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerTcpClientRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcServerTcpClientRouter).GetCommandHandler(), // Options - IpcServerAddressOption(), TcpClientAddressOption(), RuntimeTimeoutOption(), VerboseOption() + IpcServerAddressOption(), TcpClientAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption() + }; + + private static Command IpcClientTcpClientRouterCommand() => + new Command( + name: "client-client", + description: "Start a .NET application Diagnostics Server routing local IPC server <--> remote TCP server. " + + "Router is configured using an IPC client (connecting diagnostic tool IPC server) " + + "and a TCP/IP client (connecting runtime TCP server).") + { + // Handler + HandlerDescriptor.FromDelegate((DiagnosticsServerIpcServerTcpClientRouterDelegate)new DiagnosticsServerRouterCommands().RunIpcClientTcpClientRouter).GetCommandHandler(), + // Options + IpcClientAddressOption(), TcpClientAddressOption(), RuntimeTimeoutOption(), VerboseOption(), ForwardPortOption() }; private static Option IpcClientAddressOption() => @@ -118,6 +132,14 @@ private static Option VerboseOption() => Argument = new Argument(name: "verbose", getDefaultValue: () => "") }; + private static Option ForwardPortOption() => + new Option( + aliases: new[] { "--forward-port", "-fp" }, + description: "Enable port forwarding, values Android|iOS for TcpClient and only Android for TcpServer. Make sure to set ANDROID_SDK_ROOT before using this option on Android.") + { + Argument = new Argument(name: "forwardPort", getDefaultValue: () => "") + }; + private static int Main(string[] args) { StringBuilder message = new StringBuilder(); @@ -131,6 +153,7 @@ private static int Main(string[] args) .AddCommand(IpcClientTcpServerRouterCommand()) .AddCommand(IpcServerTcpServerRouterCommand()) .AddCommand(IpcServerTcpClientRouterCommand()) + .AddCommand(IpcClientTcpClientRouterCommand()) .UseDefaults() .Build(); diff --git a/src/Tools/dotnet-dsrouter/USBMuxTcpClientRouterFactory.cs b/src/Tools/dotnet-dsrouter/USBMuxTcpClientRouterFactory.cs new file mode 100644 index 0000000000..870008b163 --- /dev/null +++ b/src/Tools/dotnet-dsrouter/USBMuxTcpClientRouterFactory.cs @@ -0,0 +1,499 @@ +using System; +using System.IO; +using System.Net; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Diagnostics.Tools.DiagnosticsServerRouter +{ + internal class USBMuxInterop + { + public const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + public const string MobileDeviceLibrary = "/System/Library/PrivateFrameworks/MobileDevice.framework/MobileDevice"; + public const string LibC = "libc"; + + public const int EINTR = 4; + + public enum AMDeviceNotificationMessage : uint + { + None = 0, + Connected = 1, + Disconnected = 2, + Unsubscribed = 3 + } + + public struct AMDeviceNotificationCallbackInfo + { + public AMDeviceNotificationCallbackInfo(IntPtr device, AMDeviceNotificationMessage message) + { + this.am_device = device; + this.message = message; + } + + public IntPtr am_device; + public AMDeviceNotificationMessage message; + } + + public delegate void DeviceNotificationDelegate(ref AMDeviceNotificationCallbackInfo info); + +#region MobileDeviceLibrary + [DllImport(MobileDeviceLibrary)] + public static extern uint AMDeviceNotificationSubscribe(DeviceNotificationDelegate callback, uint unused0, uint unused1, uint unused2, out IntPtr context); + + [DllImport(MobileDeviceLibrary)] + public static extern uint AMDeviceNotificationUnsubscribe(IntPtr context); + + [DllImport(MobileDeviceLibrary)] + public static extern uint AMDeviceConnect(IntPtr device); + + [DllImport(MobileDeviceLibrary)] + public static extern uint AMDeviceDisconnect(IntPtr device); + + [DllImport(MobileDeviceLibrary)] + public static extern uint AMDeviceGetConnectionID(IntPtr device); + + [DllImport(MobileDeviceLibrary)] + public static extern int AMDeviceGetInterfaceType(IntPtr device); + + [DllImport(MobileDeviceLibrary)] + public static extern uint USBMuxConnectByPort(uint connection, ushort port, out int socketHandle); +#endregion +#region CoreFoundationLibrary + [DllImport(CoreFoundationLibrary)] + public static extern void CFRunLoopRun(); + + [DllImport(CoreFoundationLibrary)] + public static extern void CFRunLoopStop(IntPtr runLoop); + + [DllImport(CoreFoundationLibrary)] + public static extern IntPtr CFRunLoopGetCurrent(); +#endregion +#region LibC + [DllImport(LibC, SetLastError = true)] + public static extern unsafe int send(int handle, byte* buffer, IntPtr length, int flags); + + [DllImport(LibC, SetLastError = true)] + public static extern unsafe int recv(int handle, byte* buffer, IntPtr length, int flags); + + [DllImport(LibC, SetLastError = true)] + public static extern int close(int handle); +#endregion + } + + internal class USBMuxStream : Stream + { + int _handle = -1; + + public USBMuxStream(int handle) + { + _handle = handle; + } + + public bool IsOpen => _handle != -1; + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length => throw new NotImplementedException(); + + public override long Position { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public override void Flush() + { + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public override int Read(byte[] buffer, int offset, int count) + { + bool continueRead = true; + int bytesToRead = count; + int totalBytesRead = 0; + int currentBytesRead = 0; + + while (continueRead && bytesToRead - totalBytesRead > 0) + { + if (!IsOpen) + throw new EndOfStreamException(); + + unsafe + { + fixed (byte* fixedBuffer = buffer) + { + currentBytesRead = USBMuxInterop.recv(_handle, fixedBuffer + totalBytesRead, new IntPtr(bytesToRead - totalBytesRead), 0); + } + } + + if (currentBytesRead == -1 && Marshal.GetLastWin32Error() == USBMuxInterop.EINTR) + continue; + + continueRead = currentBytesRead > 0; + if (!continueRead) + break; + + totalBytesRead += currentBytesRead; + } + + return totalBytesRead; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return Task.Run(() => + { + int result = 0; + using (cancellationToken.Register(() => Close())) + { + try + { + result = Read(buffer, offset, count); + } + catch (Exception) + { + cancellationToken.ThrowIfCancellationRequested(); + result = 0; + } + } + return result; + }); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + bool continueWrite = true; + int bytesToWrite = count; + int currentBytesWritten = 0; + int totalBytesWritten = 0; + + while (continueWrite && bytesToWrite - totalBytesWritten > 0) + { + if (!IsOpen) + throw new EndOfStreamException(); + + unsafe + { + fixed (byte* fixedBuffer = buffer) + { + currentBytesWritten = USBMuxInterop.send(_handle, fixedBuffer + totalBytesWritten, new IntPtr(bytesToWrite - totalBytesWritten), 0); + } + } + + if (currentBytesWritten == -1 && Marshal.GetLastWin32Error() == USBMuxInterop.EINTR) + continue; + + continueWrite = currentBytesWritten != -1; + + if (!continueWrite) + break; + + totalBytesWritten += currentBytesWritten; + } + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return Task.Run(() => + { + using (cancellationToken.Register(() => Close())) + { + Write(buffer, offset, count); + } + }); + } + + public override void Close() + { + if (IsOpen) + { + USBMuxInterop.close(_handle); + _handle = -1; + } + } + + protected override void Dispose(bool disposing) + { + Close(); + base.Dispose(disposing); + } + } + + internal class USBMuxTcpClientRouterFactory : TcpClientRouterFactory + { + readonly int _port; + + IntPtr _device = IntPtr.Zero; + uint _deviceConnectionID = 0; + IntPtr _loopingThread = IntPtr.Zero; + + public static TcpClientRouterFactory CreateUSBMuxInstance(string tcpClient, int runtimeTimeoutMs, ILogger logger) + { + return new USBMuxTcpClientRouterFactory(tcpClient, runtimeTimeoutMs, logger); + } + + public USBMuxTcpClientRouterFactory(string tcpClient, int runtimeTimeoutMs, ILogger logger) + : base(tcpClient, runtimeTimeoutMs, logger) + { + _port = new IpcTcpSocketEndPoint(tcpClient).EndPoint.Port; + } + + public override async Task ConnectTcpStreamAsync(CancellationToken token) + { + return await ConnectTcpStreamAsyncInternal(token, _auto_shutdown).ConfigureAwait(false); + } + + public override async Task ConnectTcpStreamAsync(CancellationToken token, bool retry) + { + return await ConnectTcpStreamAsyncInternal(token, retry).ConfigureAwait(false); + } + + public override void Start() + { + // Start device subscription thread. + StartNotificationSubscribeThread(); + } + + public override void Stop() + { + // Stop device subscription thread. + StopNotificationSubscribeThread(); + } + + async Task ConnectTcpStreamAsyncInternal(CancellationToken token, bool retry) + { + int handle = -1; + ushort networkPort = (ushort)IPAddress.HostToNetworkOrder(unchecked((short)_port)); + + _logger?.LogDebug($"Connecting new tcp endpoint over usbmux \"{_tcpClientAddress}\"."); + + using var connectTimeoutTokenSource = new CancellationTokenSource(); + using var connectTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, connectTimeoutTokenSource.Token); + + connectTimeoutTokenSource.CancelAfter(TcpClientTimeoutMs); + + do + { + try + { + handle = ConnectTcpClientOverUSBMux(); + retry = false; + } + catch (Exception) + { + if (connectTimeoutTokenSource.IsCancellationRequested) + { + _logger?.LogDebug("No USB stream connected, timing out."); + + if (_auto_shutdown) + throw new RuntimeTimeoutException(TcpClientTimeoutMs); + + throw new TimeoutException(); + } + + // If we are not doing retries when runtime is unavailable, fail right away, this will + // break any accepted IPC connections, making sure client is notified and could reconnect. + // If not, retry until succeed or time out. + if (!retry) + { + _logger?.LogTrace($"Failed connecting {_port} over usbmux."); + throw; + } + + _logger?.LogTrace($"Failed connecting {_port} over usbmux, wait {TcpClientRetryTimeoutMs} ms before retrying."); + + // If we get an error (without hitting timeout above), most likely due to unavailable device/listener. + // Delay execution to prevent to rapid retry attempts. + await Task.Delay(TcpClientRetryTimeoutMs, token).ConfigureAwait(false); + } + } + while (retry); + + return new USBMuxStream(handle); + } + + int ConnectTcpClientOverUSBMux() + { + uint result = 0; + int handle = -1; + ushort networkPort = (ushort)IPAddress.HostToNetworkOrder(unchecked((short)_port)); + + lock (this) + { + if (_deviceConnectionID == 0) + throw new Exception($"Failed to connect device over USB, no device currently connected."); + + result = USBMuxInterop.USBMuxConnectByPort(_deviceConnectionID, networkPort, out handle); + } + + if (result != 0) + throw new Exception($"Failed to connect device over USB using connection {_deviceConnectionID} and port {_port}."); + + return handle; + } + + bool ConnectDevice(IntPtr newDevice) + { + if (_device != IntPtr.Zero) + return false; + + _device = newDevice; + if (USBMuxInterop.AMDeviceConnect(_device) == 0) + { + _deviceConnectionID = USBMuxInterop.AMDeviceGetConnectionID(_device); + _logger?.LogInformation($"Successfully connected new device, id={_deviceConnectionID}."); + return true; + } + else + { + _logger?.LogError($"Failed connecting new device."); + return false; + } + } + + bool DisconnectDevice() + { + if (_device != IntPtr.Zero) + { + if (_deviceConnectionID != 0) + { + USBMuxInterop.AMDeviceDisconnect(_device); + _logger?.LogInformation($"Successfully disconnected device, id={_deviceConnectionID}."); + _deviceConnectionID = 0; + } + + _device = IntPtr.Zero; + } + + return true; + } + + void AMDeviceNotificationCallback(ref USBMuxInterop.AMDeviceNotificationCallbackInfo info) + { + _logger?.LogTrace($"AMDeviceNotificationInternal callback, device={info.am_device}, action={info.message}"); + + try + { + lock (this) + { + int interfaceType = USBMuxInterop.AMDeviceGetInterfaceType(info.am_device); + switch (info.message) + { + case USBMuxInterop.AMDeviceNotificationMessage.Connected: + if (interfaceType == 1 && _device == IntPtr.Zero) + { + ConnectDevice(info.am_device); + } + else if (interfaceType == 1 && _device != IntPtr.Zero) + { + _logger?.LogInformation($"Discovered new device, but one is already connected, ignoring new device."); + } + else if (interfaceType == 0) + { + _logger?.LogInformation($"Discovered new device not connected over USB, ignoring new device."); + } + break; + case USBMuxInterop.AMDeviceNotificationMessage.Disconnected: + case USBMuxInterop.AMDeviceNotificationMessage.Unsubscribed: + if (_device == info.am_device) + { + DisconnectDevice(); + } + break; + } + } + } + catch (Exception ex) + { + _logger?.LogError($"Failed AMDeviceNotificationCallback: {ex.Message}. Failed handling device={info.am_device} using action={info.message}"); + } + } + + void AMDeviceNotificationSubscribeLoop() + { + IntPtr context = IntPtr.Zero; + + try + { + lock (this) + { + if (_loopingThread != IntPtr.Zero) + { + _logger?.LogError($"AMDeviceNotificationSubscribeLoop already running."); + throw new Exception("AMDeviceNotificationSubscribeLoop already running."); + } + + _loopingThread = USBMuxInterop.CFRunLoopGetCurrent(); + } + + _logger?.LogTrace($"Calling AMDeviceNotificationSubscribe."); + + if (USBMuxInterop.AMDeviceNotificationSubscribe(AMDeviceNotificationCallback, 0, 0, 0, out context) != 0) + { + _logger?.LogError($"Failed AMDeviceNotificationSubscribe call."); + throw new Exception("Failed AMDeviceNotificationSubscribe call."); + } + + _logger?.LogTrace($"Start dispatching notifications."); + USBMuxInterop.CFRunLoopRun(); + _logger?.LogTrace($"Stop dispatching notifications."); + } + catch (Exception ex) + { + _logger?.LogError($"Failed running subscribe loop: {ex.Message}. Disabling detection of devices connected over USB."); + } + finally + { + lock (this) + { + if (_loopingThread != IntPtr.Zero) + { + _loopingThread = IntPtr.Zero; + } + + DisconnectDevice(); + } + + if (context != IntPtr.Zero) + { + _logger?.LogTrace($"Calling AMDeviceNotificationUnsubscribe."); + USBMuxInterop.AMDeviceNotificationUnsubscribe(context); + } + } + } + + void StartNotificationSubscribeThread() + { + new Thread(new ThreadStart(() => AMDeviceNotificationSubscribeLoop())).Start(); + } + + void StopNotificationSubscribeThread() + { + lock (this) + { + if (_loopingThread != IntPtr.Zero) + USBMuxInterop.CFRunLoopStop(_loopingThread); + } + } + } +} diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs index 14c66b3fc0..f9d191e4e3 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectCommand.cs @@ -21,7 +21,7 @@ namespace Microsoft.Diagnostics.Tools.Trace { internal static class CollectCommandHandler { - delegate Task CollectDelegate(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string port, bool showchildio); + delegate Task CollectDelegate(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string port, bool showchildio, bool resumeRuntime); /// /// Collects a diagnostic trace from a currently running process or launch a child process and trace it. @@ -39,16 +39,17 @@ internal static class CollectCommandHandler /// The duration of trace to be taken. /// A list of CLR events to be emitted. /// The verbosity level of CLR events - /// Path to the diagnostic port to be created. + /// Path to the diagnostic port to be used. /// Should IO from a child process be hidden. + /// Resume runtime once session has been initialized. /// - private static async Task Collect(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string diagnosticPort, bool showchildio) + private static async Task Collect(CancellationToken ct, IConsole console, int processId, FileInfo output, uint buffersize, string providers, string profile, TraceFileFormat format, TimeSpan duration, string clrevents, string clreventlevel, string name, string diagnosticPort, bool showchildio, bool resumeRuntime) { - int ret = 0; bool collectionStopped = false; bool cancelOnEnter = true; bool cancelOnCtrlC = true; bool printStatusOverTime = true; + int ret = ReturnCode.Ok; try { @@ -79,7 +80,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i if (showchildio) { Console.WriteLine("--show-child-io must not be specified when attaching to a process"); - return ErrorCodes.ArgumentError; + return ReturnCode.ArgumentError; } if (CommandUtils.ValidateArgumentsForAttach(processId, name, diagnosticPort, out int resolvedProcessId)) { @@ -87,12 +88,12 @@ private static async Task Collect(CancellationToken ct, IConsole console, i } else { - return ErrorCodes.ArgumentError; + return ReturnCode.ArgumentError; } } else if (!CommandUtils.ValidateArgumentsForChildProcess(processId, name, diagnosticPort)) { - return ErrorCodes.ArgumentError; + return ReturnCode.ArgumentError; } if (profile.Length == 0 && providers.Length == 0 && clrevents.Length == 0) @@ -116,7 +117,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i if (selectedProfile == null) { Console.Error.WriteLine($"Invalid profile name: {profile}"); - return ErrorCodes.ArgumentError; + return ReturnCode.ArgumentError; } Profile.MergeProfileAndProviders(selectedProfile, providerCollection, enabledBy); @@ -142,53 +143,62 @@ private static async Task Collect(CancellationToken ct, IConsole console, i if (providerCollection.Count <= 0) { Console.Error.WriteLine("No providers were specified to start a trace."); - return ErrorCodes.ArgumentError; + return ReturnCode.ArgumentError; } PrintProviders(providerCollection, enabledBy); DiagnosticsClient diagnosticsClient; - Process process; + Process process = null; DiagnosticsClientBuilder builder = new DiagnosticsClientBuilder("dotnet-trace", 10); - bool shouldResumeRuntime = ProcessLauncher.Launcher.HasChildProc || !string.IsNullOrEmpty(diagnosticPort); var shouldExit = new ManualResetEvent(false); ct.Register(() => shouldExit.Set()); using (DiagnosticsClientHolder holder = await builder.Build(ct, processId, diagnosticPort, showChildIO: showchildio, printLaunchCommand: true)) { + string processMainModuleFileName = ""; + // if builder returned null, it means we received ctrl+C while waiting for clients to connect. Exit gracefully. if (holder == null) { - return await Task.FromResult(ret); + return await Task.FromResult(ReturnCode.Ok); } diagnosticsClient = holder.Client; - if (shouldResumeRuntime) + if (ProcessLauncher.Launcher.HasChildProc) { process = Process.GetProcessById(holder.EndpointInfo.ProcessId); } + else if (IpcEndpointConfig.TryParse(diagnosticPort, out IpcEndpointConfig portConfig) && (portConfig.IsConnectConfig || portConfig.IsListenConfig)) + { + // No information regarding process (could even be a routed process), + // use "file" part of IPC channel name as process main module file name. + processMainModuleFileName = Path.GetFileName(portConfig.Address); + } else { process = Process.GetProcessById(processId); } - string processMainModuleFileName = ""; - // Reading the process MainModule filename can fail if the target process closes - // or isn't fully setup. Retry a few times to attempt to address the issue - for (int attempts = 0; true; attempts++) + if (process != null) { - try - { - processMainModuleFileName = process.MainModule.FileName; - break; - } - catch + // Reading the process MainModule filename can fail if the target process closes + // or isn't fully setup. Retry a few times to attempt to address the issue + for (int attempts = 0; true; attempts++) { - if (attempts > 10) + try + { + processMainModuleFileName = process.MainModule.FileName; + break; + } + catch { - Console.Error.WriteLine("Unable to examine process."); - return ErrorCodes.SessionCreationError; + if (attempts > 10) + { + Console.Error.WriteLine("Unable to examine process."); + return ReturnCode.SessionCreationError; + } + Thread.Sleep(200); } - Thread.Sleep(200); } } @@ -210,9 +220,16 @@ private static async Task Collect(CancellationToken ct, IConsole console, i try { session = diagnosticsClient.StartEventPipeSession(providerCollection, true, (int)buffersize); - if (shouldResumeRuntime) + if (resumeRuntime) { - diagnosticsClient.ResumeRuntime(); + try + { + diagnosticsClient.ResumeRuntime(); + } + catch (UnsupportedCommandException) + { + // Noop if command is unsupported, since the target is most likely a 3.1 app. + } } } catch (DiagnosticsClientException e) @@ -223,7 +240,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i if (session == null) { Console.Error.WriteLine("Unable to create session."); - return ErrorCodes.SessionCreationError; + return ReturnCode.SessionCreationError; } if (shouldStopAfterDuration) @@ -312,7 +329,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i // If the process is shutting down by itself print the return code from the process. // Capture this before leaving the using, as the Dispose of the DiagnosticsClientHolder // may terminate the target process causing it to have the wrong error code - if (ProcessLauncher.Launcher.ChildProc.WaitForExit(5000)) + if (ProcessLauncher.Launcher.HasChildProc && ProcessLauncher.Launcher.ChildProc.WaitForExit(5000)) { ret = ProcessLauncher.Launcher.ChildProc.ExitCode; Console.WriteLine($"Process exited with code '{ret}'."); @@ -324,8 +341,8 @@ private static async Task Collect(CancellationToken ct, IConsole console, i catch (Exception ex) { Console.Error.WriteLine($"[ERROR] {ex.ToString()}"); - ret = ErrorCodes.TracingError; collectionStopped = true; + ret = ReturnCode.TracingError; } finally { @@ -339,7 +356,7 @@ private static async Task Collect(CancellationToken ct, IConsole console, i { if (!collectionStopped || ct.IsCancellationRequested) { - ret = ErrorCodes.TracingError; + ret = ReturnCode.TracingError; } // If we launched a child proc that hasn't exited yet, terminate it before we exit. @@ -399,7 +416,8 @@ public static Command CollectCommand() => CLREventLevelOption(), CommonOptions.NameOption(), DiagnosticPortOption(), - ShowChildIOOption() + ShowChildIOOption(), + ResumeRuntimeOption() }; private static uint DefaultCircularBufferSizeInMB() => 256; @@ -471,7 +489,7 @@ private static Option CLREventLevelOption() => private static Option DiagnosticPortOption() => new Option( alias: "--diagnostic-port", - description: @"The path to a diagnostic port to be created.") + description: @"The path to a diagnostic port to be used.") { Argument = new Argument(name: "diagnosticPort", getDefaultValue: () => string.Empty) }; @@ -482,5 +500,13 @@ private static Option ShowChildIOOption() => { Argument = new Argument(name: "show-child-io", getDefaultValue: () => false) }; + + private static Option ResumeRuntimeOption() => + new Option( + alias: "--resume-runtime", + description: @"Resume runtime once session has been initialized, defaults to true. Disable resume of runtime using --resume-runtime:false") + { + Argument = new Argument(name: "resumeRuntime", getDefaultValue: () => true) + }; } } diff --git a/src/inc/sospriv.idl b/src/inc/sospriv.idl index 332ec79c50..102801f7e5 100644 --- a/src/inc/sospriv.idl +++ b/src/inc/sospriv.idl @@ -438,3 +438,14 @@ interface ISOSDacInterface10 : IUnknown HRESULT IsComWrappersRCW(CLRDATA_ADDRESS rcw, BOOL *isComWrappersRCW); HRESULT GetComWrappersRCWData(CLRDATA_ADDRESS rcw, CLRDATA_ADDRESS *identity); } + +[ + object, + local, + uuid(96BA1DB9-14CD-4492-8065-1CAAECF6E5CF) +] +interface ISOSDacInterface11 : IUnknown +{ + HRESULT IsTrackedType(CLRDATA_ADDRESS objAddr, BOOL* isTrackedType, BOOL* hasTaggedMemory); + HRESULT GetTaggedMemory(CLRDATA_ADDRESS objAddr, CLRDATA_ADDRESS* taggedMemory, size_t* taggedMemorySizeInBytes); +} diff --git a/src/pal/prebuilt/idl/sospriv_i.cpp b/src/pal/prebuilt/idl/sospriv_i.cpp index 214e5bee2c..8f0be3732f 100644 --- a/src/pal/prebuilt/idl/sospriv_i.cpp +++ b/src/pal/prebuilt/idl/sospriv_i.cpp @@ -109,6 +109,9 @@ MIDL_DEFINE_GUID(IID, IID_ISOSDacInterface9,0x4eca42d8,0x7e7b,0x4c8a,0xa1,0x16,0 MIDL_DEFINE_GUID(IID, IID_ISOSDacInterface10,0x90B8FCC3,0x7251,0x4B0A,0xAE,0x3D,0x5C,0x13,0xA6,0x7E,0xC9,0xAA); +MIDL_DEFINE_GUID(IID, IID_ISOSDacInterface11,0x96BA1DB9,0x14CD,0x4492,0x80,0x65,0x1C,0xAA,0xEC,0xF6,0xE5,0xCF); + + #undef MIDL_DEFINE_GUID #ifdef __cplusplus diff --git a/src/pal/prebuilt/inc/sospriv.h b/src/pal/prebuilt/inc/sospriv.h index 1cfc2a1f34..de72fa2c95 100644 --- a/src/pal/prebuilt/inc/sospriv.h +++ b/src/pal/prebuilt/inc/sospriv.h @@ -2898,6 +2898,103 @@ EXTERN_C const IID IID_ISOSDacInterface10; #endif /* __ISOSDacInterface10_INTERFACE_DEFINED__ */ +#ifndef __ISOSDacInterface11_INTERFACE_DEFINED__ +#define __ISOSDacInterface11_INTERFACE_DEFINED__ + +/* interface ISOSDacInterface11 */ +/* [uuid][local][object] */ + + +EXTERN_C const IID IID_ISOSDacInterface11; + +#if defined(__cplusplus) && !defined(CINTERFACE) + + MIDL_INTERFACE("96BA1DB9-14CD-4492-8065-1CAAECF6E5CF") + ISOSDacInterface11 : public IUnknown + { + public: + virtual HRESULT STDMETHODCALLTYPE IsTrackedType( + CLRDATA_ADDRESS objAddr, + BOOL *isTrackedType, + BOOL *hasTaggedMemory) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetTaggedMemory( + CLRDATA_ADDRESS objAddr, + CLRDATA_ADDRESS *taggedMemory, + size_t *taggedMemorySizeInBytes) = 0; + + }; + + +#else /* C style interface */ + + typedef struct ISOSDacInterface11Vtbl + { + BEGIN_INTERFACE + + HRESULT ( STDMETHODCALLTYPE *QueryInterface )( + ISOSDacInterface11 * This, + /* [in] */ REFIID riid, + /* [annotation][iid_is][out] */ + _COM_Outptr_ void **ppvObject); + + ULONG ( STDMETHODCALLTYPE *AddRef )( + ISOSDacInterface11 * This); + + ULONG ( STDMETHODCALLTYPE *Release )( + ISOSDacInterface11 * This); + + HRESULT ( STDMETHODCALLTYPE *IsTrackedType )( + ISOSDacInterface11 * This, + CLRDATA_ADDRESS objAddr, + BOOL *isTrackedType, + BOOL *hasTaggedMemory); + + HRESULT ( STDMETHODCALLTYPE *GetTaggedMemory )( + ISOSDacInterface11 * This, + CLRDATA_ADDRESS objAddr, + CLRDATA_ADDRESS *taggedMemory, + size_t *taggedMemorySizeInBytes); + + END_INTERFACE + } ISOSDacInterface11Vtbl; + + interface ISOSDacInterface11 + { + CONST_VTBL struct ISOSDacInterface11Vtbl *lpVtbl; + }; + + + +#ifdef COBJMACROS + + +#define ISOSDacInterface11_QueryInterface(This,riid,ppvObject) \ + ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) + +#define ISOSDacInterface11_AddRef(This) \ + ( (This)->lpVtbl -> AddRef(This) ) + +#define ISOSDacInterface11_Release(This) \ + ( (This)->lpVtbl -> Release(This) ) + + +#define ISOSDacInterface11_IsTrackedType(This,objAddr,isTrackedType,hasTaggedMemory) \ + ( (This)->lpVtbl -> IsTrackedType(This,objAddr,isTrackedType,hasTaggedMemory) ) + +#define ISOSDacInterface11_GetTaggedMemory(This,objAddr,taggedMemory,taggedMemorySizeInBytes) \ + ( (This)->lpVtbl -> GetTaggedMemory(This,objAddr,taggedMemory,taggedMemorySizeInBytes) ) + +#endif /* COBJMACROS */ + + +#endif /* C style interface */ + + + + +#endif /* __ISOSDacInterface11_INTERFACE_DEFINED__ */ + /* Additional Prototypes for ALL interfaces */ diff --git a/src/pal/src/locale/utf8.cpp b/src/pal/src/locale/utf8.cpp index 38599e6b08..96a633b165 100644 --- a/src/pal/src/locale/utf8.cpp +++ b/src/pal/src/locale/utf8.cpp @@ -1,6 +1,5 @@ // 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. /*++ @@ -11,7 +10,7 @@ Module Name: unicode/utf8.c Abstract: - Functions to encode and decode UTF-8 strings. This is a port of the C# version from mscorlib. + Functions to encode and decode UTF-8 strings. This is a port of the C# version from Utf8Encoding.cs. Revision History: @@ -26,6 +25,10 @@ using namespace CorUnix; #define FASTLOOP +#ifndef COUNTOF +#define COUNTOF(x) (sizeof(x) / sizeof((x)[0])) +#endif + struct CharUnicodeInfo { static const WCHAR HIGH_SURROGATE_START = 0xd800; @@ -48,7 +51,7 @@ struct Char return (c & 0xFC00) == CharUnicodeInfo::LOW_SURROGATE_START; } - // Test if the wide character is a low surrogate + // Test if the wide character is a surrogate half static bool IsSurrogate(const WCHAR c) { return (c & 0xF800) == CharUnicodeInfo::HIGH_SURROGATE_START; @@ -66,7 +69,7 @@ struct Char return IsLowSurrogate(s[index]); } - // Test if the wide character is a low surrogate + // Test if the wide character is a surrogate half static bool IsSurrogate(const WCHAR* s, int index) { return IsSurrogate(s[index]); @@ -229,7 +232,7 @@ class DecoderReplacementFallback : public DecoderFallback if (bFoundHigh) throw ArgumentException("String 'replacement' contains invalid Unicode code points.", "replacement"); - wcscpy_s(strDefault, sizeof(strDefault), replacement); + wcscpy_s(strDefault, COUNTOF(strDefault), replacement); strDefaultLength = replacementLength; } @@ -331,7 +334,7 @@ class DecoderFallbackBuffer else { // Low surrogate - if (bHighSurrogate == false) + if (!bHighSurrogate) throw ArgumentException("String 'chars' contains invalid Unicode code points."); bHighSurrogate = false; } @@ -387,7 +390,7 @@ class DecoderFallbackBuffer else { // Low surrogate - if (bHighSurrogate == false) + if (!bHighSurrogate) throw ArgumentException("String 'chars' contains invalid Unicode code points."); bHighSurrogate = false; } @@ -426,7 +429,7 @@ class DecoderReplacementFallbackBuffer : public DecoderFallbackBuffer // Construction DecoderReplacementFallbackBuffer(DecoderReplacementFallback* fallback) { - wcscpy_s(strDefault, sizeof(strDefault), fallback->GetDefaultString()); + wcscpy_s(strDefault, COUNTOF(strDefault), fallback->GetDefaultString()); strDefaultLength = PAL_wcslen((const WCHAR *)fallback->GetDefaultString()); } @@ -615,7 +618,7 @@ class EncoderFallbackException : public ArgumentException WCHAR GetCharUnknownHigh() { return (charUnknownHigh); - } + } WCHAR GetCharUnknownLow() { @@ -706,7 +709,7 @@ class EncoderReplacementFallback : public EncoderFallback if (bFoundHigh) throw ArgumentException("String 'replacement' contains invalid Unicode code points.", "replacement"); - wcscpy_s(strDefault, sizeof(strDefault), replacement); + wcscpy_s(strDefault, COUNTOF(strDefault), replacement); strDefaultLength = replacementLength; } @@ -878,8 +881,8 @@ class EncoderReplacementFallbackBuffer : public EncoderFallbackBuffer EncoderReplacementFallbackBuffer(EncoderReplacementFallback* fallback) { // 2X in case we're a surrogate pair - wcscpy_s(strDefault, sizeof(strDefault), fallback->GetDefaultString()); - wcscat_s(strDefault, sizeof(strDefault), fallback->GetDefaultString()); + wcscpy_s(strDefault, COUNTOF(strDefault), fallback->GetDefaultString()); + wcscat_s(strDefault, COUNTOF(strDefault), fallback->GetDefaultString()); strDefaultLength = 2 * PAL_wcslen((const WCHAR *)fallback->GetDefaultString()); } @@ -1134,8 +1137,8 @@ class UTF8Encoding { // Get our byte[] BYTE* pStart = *pSrc; - BYTE* bytesUnknown; - int size = GetBytesUnknown(pStart, ch, &bytesUnknown); + BYTE bytesUnknown[3]; + int size = GetBytesUnknown(pStart, ch, bytesUnknown); // Do the actual fallback if (!fallback->InternalFallback(bytesUnknown, *pSrc, pTarget, size)) @@ -1152,8 +1155,8 @@ class UTF8Encoding int FallbackInvalidByteSequence(BYTE* pSrc, int ch, DecoderFallbackBuffer *fallback) { // Get our byte[] - BYTE *bytesUnknown; - int size = GetBytesUnknown(pSrc, ch, &bytesUnknown); + BYTE bytesUnknown[3]; + int size = GetBytesUnknown(pSrc, ch, bytesUnknown); // Do the actual fallback int count = fallback->InternalFallback(bytesUnknown, pSrc, size); @@ -1164,24 +1167,23 @@ class UTF8Encoding return count; } - int GetBytesUnknown(BYTE* pSrc, int ch, BYTE **bytesUnknown) + int GetBytesUnknown(BYTE* pSrc, int ch, BYTE* bytesUnknown) { int size; - BYTE bytes[3]; // See if it was a plain char // (have to check >= 0 because we have all sorts of wierd bit flags) if (ch < 0x100 && ch >= 0) { pSrc--; - bytes[0] = (BYTE)ch; + bytesUnknown[0] = (BYTE)ch; size = 1; } // See if its an unfinished 2 byte sequence else if ((ch & (SupplimentarySeq | ThreeByteSeq)) == 0) { pSrc--; - bytes[0] = (BYTE)((ch & 0x1F) | 0xc0); + bytesUnknown[0] = (BYTE)((ch & 0x1F) | 0xc0); size = 1; } // So now we're either 2nd byte of 3 or 4 byte sequence or @@ -1194,24 +1196,24 @@ class UTF8Encoding { // 3rd byte of 4 byte sequence pSrc -= 3; - bytes[0] = (BYTE)(((ch >> 12) & 0x07) | 0xF0); - bytes[1] = (BYTE)(((ch >> 6) & 0x3F) | 0x80); - bytes[2] = (BYTE)(((ch)& 0x3F) | 0x80); + bytesUnknown[0] = (BYTE)(((ch >> 12) & 0x07) | 0xF0); + bytesUnknown[1] = (BYTE)(((ch >> 6) & 0x3F) | 0x80); + bytesUnknown[2] = (BYTE)(((ch)& 0x3F) | 0x80); size = 3; } else if ((ch & (FinalByte >> 12)) != 0) { // 2nd byte of a 4 byte sequence pSrc -= 2; - bytes[0] = (BYTE)(((ch >> 6) & 0x07) | 0xF0); - bytes[1] = (BYTE)(((ch)& 0x3F) | 0x80); + bytesUnknown[0] = (BYTE)(((ch >> 6) & 0x07) | 0xF0); + bytesUnknown[1] = (BYTE)(((ch)& 0x3F) | 0x80); size = 2; } else { // 4th byte of a 4 byte sequence pSrc--; - bytes[0] = (BYTE)(((ch)& 0x07) | 0xF0); + bytesUnknown[0] = (BYTE)(((ch)& 0x07) | 0xF0); size = 1; } } @@ -1222,20 +1224,19 @@ class UTF8Encoding { // So its 2nd byte of a 3 byte sequence pSrc -= 2; - bytes[0] = (BYTE)(((ch >> 6) & 0x0F) | 0xE0); - bytes[1] = (BYTE)(((ch)& 0x3F) | 0x80); + bytesUnknown[0] = (BYTE)(((ch >> 6) & 0x0F) | 0xE0); + bytesUnknown[1] = (BYTE)(((ch)& 0x3F) | 0x80); size = 2; } else { // 1st byte of a 3 byte sequence pSrc--; - bytes[0] = (BYTE)(((ch)& 0x0F) | 0xE0); + bytesUnknown[0] = (BYTE)(((ch)& 0x0F) | 0xE0); size = 1; } } - *bytesUnknown = bytes; return size; } @@ -1280,7 +1281,7 @@ class UTF8Encoding int ch = 0; DecoderFallbackBuffer *fallback = nullptr; - for (;;) + while (true) { // SLOWLOOP: does all range checks, handles all special cases, but it is slow if (pSrc >= pEnd) { @@ -1463,7 +1464,7 @@ class UTF8Encoding } // get pSrc 2-byte aligned - if (((int)pSrc & 0x1) != 0) { + if (((size_t)pSrc & 0x1) != 0) { ch = *pSrc; pSrc++; if (ch > 0x7F) { @@ -1472,7 +1473,7 @@ class UTF8Encoding } // get pSrc 4-byte aligned - if (((int)pSrc & 0x2) != 0) { + if (((size_t)pSrc & 0x2) != 0) { ch = *(USHORT*)pSrc; if ((ch & 0x8080) != 0) { goto LongCodeWithMask16; @@ -1650,7 +1651,7 @@ class UTF8Encoding DecoderFallbackBuffer *fallback = nullptr; - for (;;) + while (true) { // SLOWLOOP: does all range checks, handles all special cases, but it is slow @@ -1737,7 +1738,7 @@ class UTF8Encoding fallback = decoderFallback->CreateFallbackBuffer(); fallback->InternalInitialize(bytes, pAllocatedBufferEnd); } - + // That'll back us up the appropriate # of bytes if we didn't get anywhere if (!FallbackInvalidByteSequence(&pSrc, ch, fallback, &pTarget)) { @@ -1835,7 +1836,7 @@ class UTF8Encoding pSrc--; // Throw that we don't have enough room (pSrc could be < chars if we had started to process - // a 4 byte sequence alredy) + // a 4 byte sequence already) Contract::Assert(pSrc >= bytes || pTarget == chars, "[UTF8Encoding.GetChars]Expected pSrc to be within input buffer or throw due to no output]"); ThrowCharsOverflow(pTarget == chars); @@ -1902,7 +1903,7 @@ class UTF8Encoding pTarget++; // get pSrc to be 2-byte aligned - if ((((int)pSrc) & 0x1) != 0) { + if ((((size_t)pSrc) & 0x1) != 0) { ch = *pSrc; pSrc++; if (ch > 0x7F) { @@ -1913,7 +1914,7 @@ class UTF8Encoding } // get pSrc to be 4-byte aligned - if ((((int)pSrc) & 0x2) != 0) { + if ((((size_t)pSrc) & 0x2) != 0) { ch = *(USHORT*)pSrc; if ((ch & 0x8080) != 0) { goto LongCodeWithMask16; @@ -2040,7 +2041,7 @@ class UTF8Encoding // extra byte, we're already planning 2 chars for 2 of these bytes, // but the big loop is testing the target against pStop, so we need - // to subtract 2 more or we risk overrunning the input. Subtract + // to subtract 2 more or we risk overrunning the input. Subtract // one here and one below. pStop--; } @@ -2157,7 +2158,7 @@ class UTF8Encoding // assume that JIT will enregister pSrc, pTarget and ch - for (;;) { + while (true) { // SLOWLOOP: does all range checks, handles all special cases, but it is slow if (pSrc >= pEnd) { @@ -2522,7 +2523,7 @@ class UTF8Encoding ch = 0; } - InternalDelete(fallbackBuffer); + InternalDelete(fallbackBuffer); return (int)(pTarget - bytes); } @@ -2540,7 +2541,7 @@ class UTF8Encoding int ch = 0; - for (;;) { + while (true) { // SLOWLOOP: does all range checks, handles all special cases, but it is slow if (pSrc >= pEnd) { @@ -2736,7 +2737,7 @@ class UTF8Encoding } // get pSrc aligned - if (((int)pSrc & 0x2) != 0) { + if (((size_t)pSrc & 0x2) != 0) { ch = *pSrc; pSrc++; if (ch > 0x7F) // Not ASCII diff --git a/src/pal/src/misc/sysinfo.cpp b/src/pal/src/misc/sysinfo.cpp index 45b6b3bdb8..214e8ccd38 100644 --- a/src/pal/src/misc/sysinfo.cpp +++ b/src/pal/src/misc/sysinfo.cpp @@ -35,6 +35,10 @@ Revision History: #error Either sysctl or sysconf is required for GetSystemInfo. #endif +#if defined(__FreeBSD__) +#include +#endif + #if HAVE_SYSINFO #include #endif diff --git a/src/singlefile-tools.proj b/src/singlefile-tools.proj index 5435072a13..0c3873fe83 100644 --- a/src/singlefile-tools.proj +++ b/src/singlefile-tools.proj @@ -10,7 +10,7 @@ - <_ProjectToBundle Include="$(RepoRoot)src/Tools/**/*.csproj" Exclude="$(RepoRoot)src/Tools/dotnet-monitor/**/*.csproj;$(RepoRoot)src/Tools/dotnet-analyze/**/*.csproj" /> + <_ProjectToBundle Include="$(RepoRoot)src/Tools/**/*.csproj" Exclude="$(RepoRoot)src/Tools/dotnet-dsrouter/**/*.csproj" /> diff --git a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/TestStreamingLogger.cs b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/TestStreamingLogger.cs index c1075da38f..f61240dcaa 100644 --- a/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/TestStreamingLogger.cs +++ b/src/tests/Microsoft.Diagnostics.Monitoring.EventPipe/TestStreamingLogger.cs @@ -10,7 +10,7 @@ namespace Microsoft.Diagnostics.Monitoring.EventPipe.UnitTests { /// - /// CONSIDER We can't reuse StreamingLoggerProvider from using Microsoft.Diagnostics.Monitoring.RestServer. + /// CONSIDER We can't reuse StreamingLoggerProvider from using Microsoft.Diagnostics.Monitoring.WebApi. /// Adding a reference to that project causes assembly resolution issues. /// internal sealed class TestStreamingLoggerProvider : ILoggerProvider diff --git a/src/tests/Microsoft.Diagnostics.Monitoring/CommandLineHelperTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring/CommandLineHelperTests.cs deleted file mode 100644 index 2bd81cfa83..0000000000 --- a/src/tests/Microsoft.Diagnostics.Monitoring/CommandLineHelperTests.cs +++ /dev/null @@ -1,44 +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.Monitoring; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostics.Monitoring.UnitTests -{ - public class CommandLineHelperTests - { - private readonly ITestOutputHelper _output; - - public CommandLineHelperTests(ITestOutputHelper output) - { - _output = output; - } - - [Theory] - [InlineData(true, null, null)] - [InlineData(true, "", "")] - [InlineData(true, @"C:\NoArgs\test.exe", @"C:\NoArgs\test.exe")] - [InlineData(true, @"C:\WithArgs\test.exe arg1 arg2", @"C:\WithArgs\test.exe")] - [InlineData(true, @"""C:\With Space No Args\test.exe""", @"C:\With Space No Args\test.exe")] - [InlineData(true, @"""C:\With Space With Args\test.exe"" arg1 arg2", @"C:\With Space With Args\test.exe")] - [InlineData(true, @"C:\With'Quotes'No'Args\test.exe", @"C:\With'Quotes'No'Args\test.exe")] - [InlineData(true, @"C:\With'Quotes'With'Args\test.exe arg1 arg2", @"C:\With'Quotes'With'Args\test.exe")] - [InlineData(false, null, null)] - [InlineData(false, "", "")] - [InlineData(false, "/home/noargs/test", "/home/noargs/test")] - [InlineData(false, "/home/withargs/test arg1 arg2", "/home/withargs/test")] - [InlineData(false, @"""/home/with space no args/test""", "/home/with space no args/test")] - [InlineData(false, @"""/home/with space with args/test"" arg1 arg2", "/home/with space with args/test")] - [InlineData(false, @"""/home/escaped\\backslashes\\no\\args/test""", @"/home/escaped\backslashes\no\args/test")] - [InlineData(false, @"""/home/escaped\\backslashes\\with\\args/test"" arg1 arg2", @"/home/escaped\backslashes\with\args/test")] - [InlineData(false, @"""/home/escaped\""quotes\""no\""args/test""", @"/home/escaped""quotes""no""args/test")] - [InlineData(false, @"""/home/escaped\""quotes\""with\""args/test"" arg1 arg2", @"/home/escaped""quotes""with""args/test")] - public void CommandLineValidPathTest(bool isWindows, string commandLine, string expectedProcessPath) - { - Assert.Equal(expectedProcessPath, CommandLineHelper.ExtractExecutablePath(commandLine, isWindows)); - } - } -} diff --git a/src/tests/Microsoft.Diagnostics.Monitoring/EndpointInfoSourceTests.cs b/src/tests/Microsoft.Diagnostics.Monitoring/EndpointInfoSourceTests.cs deleted file mode 100644 index c1f8871b36..0000000000 --- a/src/tests/Microsoft.Diagnostics.Monitoring/EndpointInfoSourceTests.cs +++ /dev/null @@ -1,294 +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 System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Diagnostics.NETCore.Client; -using Microsoft.Diagnostics.NETCore.Client.UnitTests; -using Xunit; -using Xunit.Abstractions; - -namespace Microsoft.Diagnostics.Monitoring.UnitTests -{ - public class EndpointInfoSourceTests - { - // Generous timeout to allow APIs to respond on slower or more constrained machines - private static readonly TimeSpan DefaultPositiveVerificationTimeout = TimeSpan.FromSeconds(30); - private static readonly TimeSpan DefaultNegativeVerificationTimeout = TimeSpan.FromSeconds(2); - - private readonly ITestOutputHelper _outputHelper; - - public EndpointInfoSourceTests(ITestOutputHelper outputHelper) - { - _outputHelper = outputHelper; - } - - /// - /// Tests that other methods throw if - /// is not called. - /// - [Fact] - public async Task ServerSourceNoStartTest() - { - await using var source = CreateServerSource(out string transportName); - // Intentionally do not call Start - - using CancellationTokenSource cancellation = new CancellationTokenSource(DefaultNegativeVerificationTimeout); - - await Assert.ThrowsAsync( - () => source.GetEndpointInfoAsync(cancellation.Token)); - } - - /// - /// Tests that the server endpoint info source has not connections if no processes connect to it. - /// - [Fact] - public async Task ServerSourceNoConnectionsTest() - { - await using var source = CreateServerSource(out _); - source.Start(); - - var endpointInfos = await GetEndpointInfoAsync(source); - Assert.Empty(endpointInfos); - } - - /// - /// Tests that server endpoint info source should throw ObjectDisposedException - /// from API surface after being disposed. - /// - [Fact] - public async Task ServerSourceThrowsWhenDisposedTest() - { - var source = CreateServerSource(out _); - source.Start(); - - await source.DisposeAsync(); - - // Validate source surface throws after disposal - Assert.Throws( - () => source.Start()); - - Assert.Throws( - () => source.Start(1)); - - using var cancellation = new CancellationTokenSource(DefaultNegativeVerificationTimeout); - await Assert.ThrowsAsync( - () => source.GetEndpointInfoAsync(cancellation.Token)); - } - - /// - /// Tests that server endpoint info source should throw an exception from - /// and - /// after listening was already started. - /// - [Fact] - public async Task ServerSourceThrowsWhenMultipleStartTest() - { - await using var source = CreateServerSource(out _); - source.Start(); - - Assert.Throws( - () => source.Start()); - - Assert.Throws( - () => source.Start(1)); - } - - /// - /// Tests that the server endpoint info source can properly enumerate endpoint infos when a single - /// target connects to it and "disconnects" from it. - /// - [Fact] - public async Task ServerSourceAddRemoveSingleConnectionTest() - { - await using var source = CreateServerSource(out string transportName); - source.Start(); - - var endpointInfos = await GetEndpointInfoAsync(source); - Assert.Empty(endpointInfos); - - Task newEndpointInfoTask = source.WaitForNewEndpointInfoAsync(DefaultPositiveVerificationTimeout); - - await using (var execution1 = StartTraceeProcess("LoggerRemoteTest", transportName)) - { - await newEndpointInfoTask; - - execution1.SendSignal(); - - endpointInfos = await GetEndpointInfoAsync(source); - - var endpointInfo = Assert.Single(endpointInfos); - Assert.NotNull(endpointInfo.CommandLine); - Assert.NotNull(endpointInfo.OperatingSystem); - Assert.NotNull(endpointInfo.ProcessArchitecture); - VerifyConnection(execution1.TestRunner, endpointInfo); - - _outputHelper.WriteLine("Stopping tracee."); - } - - await Task.Delay(TimeSpan.FromSeconds(1)); - - endpointInfos = await GetEndpointInfoAsync(source); - - Assert.Empty(endpointInfos); - } - - /// - /// Tests that the server endpoint info source can properly enumerate endpoint infos when multiple - /// targets connect to it and "disconnect" from it. - /// - [Fact] - public async Task ServerSourceAddRemoveMultipleConnectionTest() - { - await using var source = CreateServerSource(out string transportName); - source.Start(); - - var endpointInfos = await GetEndpointInfoAsync(source); - Assert.Empty(endpointInfos); - - const int appCount = 5; - RemoteTestExecution[] executions = new RemoteTestExecution[appCount]; - - try - { - // Start all app instances - for (int i = 0; i < appCount; i++) - { - Task newEndpointInfoTask = source.WaitForNewEndpointInfoAsync(DefaultPositiveVerificationTimeout); - - executions[i] = StartTraceeProcess("LoggerRemoteTest", transportName); - - await newEndpointInfoTask; - - executions[i].SendSignal(); - } - - await Task.Delay(TimeSpan.FromSeconds(1)); - - endpointInfos = await GetEndpointInfoAsync(source); - - Assert.Equal(appCount, endpointInfos.Count()); - - for (int i = 0; i < appCount; i++) - { - IEndpointInfo endpointInfo = endpointInfos.FirstOrDefault(info => info.ProcessId == executions[i].TestRunner.Pid); - Assert.NotNull(endpointInfo); - Assert.NotNull(endpointInfo.CommandLine); - Assert.NotNull(endpointInfo.OperatingSystem); - Assert.NotNull(endpointInfo.ProcessArchitecture); - - VerifyConnection(executions[i].TestRunner, endpointInfo); - } - } - finally - { - _outputHelper.WriteLine("Stopping tracees."); - - int executionCount = 0; - for (int i = 0; i < appCount; i++) - { - if (null != executions[i]) - { - executionCount++; - await executions[i].DisposeAsync(); - } - } - Assert.Equal(appCount, executionCount); - } - - await Task.Delay(TimeSpan.FromSeconds(1)); - - endpointInfos = await GetEndpointInfoAsync(source); - - Assert.Empty(endpointInfos); - } - - private TestServerEndpointInfoSource CreateServerSource(out string transportName) - { - transportName = ReversedServerHelper.CreateServerTransportName(); - _outputHelper.WriteLine("Starting server endpoint info source at '" + transportName + "'."); - return new TestServerEndpointInfoSource(transportName, _outputHelper); - } - - private RemoteTestExecution StartTraceeProcess(string loggerCategory, string transportName = null) - { - _outputHelper.WriteLine("Starting tracee."); - string exePath = CommonHelper.GetTraceePathWithArgs("EventPipeTracee", targetFramework: "net5.0"); - return RemoteTestExecution.StartProcess(exePath + " " + loggerCategory, _outputHelper, transportName); - } - - private async Task> GetEndpointInfoAsync(ServerEndpointInfoSource source) - { - _outputHelper.WriteLine("Getting endpoint infos."); - using CancellationTokenSource cancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - return await source.GetEndpointInfoAsync(cancellationSource.Token); - } - - /// - /// Verifies basic information on the connection and that it matches the target process from the runner. - /// - private static void VerifyConnection(TestRunner runner, IEndpointInfo endpointInfo) - { - Assert.NotNull(runner); - Assert.NotNull(endpointInfo); - Assert.Equal(runner.Pid, endpointInfo.ProcessId); - Assert.NotEqual(Guid.Empty, endpointInfo.RuntimeInstanceCookie); - Assert.NotNull(endpointInfo.Endpoint); - } - - private sealed class TestServerEndpointInfoSource : ServerEndpointInfoSource - { - private readonly ITestOutputHelper _outputHelper; - private readonly List> _addedEndpointInfoSources = new List>(); - - public TestServerEndpointInfoSource(string transportPath, ITestOutputHelper outputHelper) - : base(transportPath) - { - _outputHelper = outputHelper; - } - - public async Task WaitForNewEndpointInfoAsync(TimeSpan timeout) - { - TaskCompletionSource addedEndpointInfoSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - using var timeoutCancellation = new CancellationTokenSource(); - var token = timeoutCancellation.Token; - using var _ = token.Register(() => addedEndpointInfoSource.TrySetCanceled(token)); - - lock (_addedEndpointInfoSources) - { - _addedEndpointInfoSources.Add(addedEndpointInfoSource); - } - - _outputHelper.WriteLine("Waiting for new endpoint info."); - timeoutCancellation.CancelAfter(timeout); - EndpointInfo endpointInfo = await addedEndpointInfoSource.Task; - _outputHelper.WriteLine("Notified of new endpoint info."); - - return endpointInfo; - } - - internal override void OnAddedEndpointInfo(EndpointInfo info) - { - _outputHelper.WriteLine($"Added endpoint info to collection: {info.DebuggerDisplay}"); - - lock (_addedEndpointInfoSources) - { - foreach (var source in _addedEndpointInfoSources) - { - source.TrySetResult(info); - } - _addedEndpointInfoSources.Clear(); - } - } - - internal override void OnRemovedEndpointInfo(EndpointInfo info) - { - _outputHelper.WriteLine($"Removed endpoint info from collection: {info.DebuggerDisplay}"); - } - } - } -} diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/GetProcessInfoTests.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/GetProcessInfoTests.cs new file mode 100644 index 0000000000..da832fa6d4 --- /dev/null +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/GetProcessInfoTests.cs @@ -0,0 +1,59 @@ +// 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; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.NETCore.Client +{ + public class GetProcessInfoTests + { + private readonly ITestOutputHelper output; + + public GetProcessInfoTests(ITestOutputHelper outputHelper) + { + output = outputHelper; + } + + [Fact] + public void BasicProcessInfoTest() + { + using TestRunner runner = new TestRunner(CommonHelper.GetTraceePathWithArgs(targetFramework: "net5.0"), output); + runner.Start(); + + try + { + DiagnosticsClient client = new DiagnosticsClient(runner.Pid); + + ProcessInfo processInfo = client.GetProcessInfo(); + + Assert.NotNull(processInfo); + Assert.Equal(runner.Pid, (int)processInfo.ProcessId); + Assert.NotNull(processInfo.CommandLine); + Assert.NotNull(processInfo.OperatingSystem); + Assert.NotNull(processInfo.ProcessArchitecture); + //Assert.Equal("Tracee", processInfo.ManagedEntrypointAssemblyName); + //Version clrVersion = ParseVersionRemoveLabel(processInfo.ClrProductVersionString); + //Assert.True(clrVersion >= new Version(6, 0, 0)); + } + finally + { + runner.PrintStatus(); + } + } + + private static Version ParseVersionRemoveLabel(string versionString) + { + Assert.NotNull(versionString); + int prereleaseLabelIndex = versionString.IndexOf('-'); + if (prereleaseLabelIndex >= 0) + { + versionString = versionString.Substring(0, prereleaseLabelIndex); + } + return Version.Parse(versionString); + } + } +} diff --git a/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs b/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs index 4b37afe627..88fa0014ee 100644 --- a/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs +++ b/src/tests/Microsoft.Diagnostics.NETCore.Client/TestRunner.cs @@ -4,6 +4,7 @@ using System; +using System.Linq; using System.ComponentModel; using System.Diagnostics; using System.IO; @@ -11,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Xunit.Abstractions; +using System.Collections.Generic; namespace Microsoft.Diagnostics.NETCore.Client { @@ -22,13 +24,14 @@ public class TestRunner : IDisposable private CancellationTokenSource cts; public TestRunner(string testExePath, ITestOutputHelper _outputHelper = null, - bool redirectError = false, bool redirectInput = false) + bool redirectError = false, bool redirectInput = false, Dictionary envVars = null) { startInfo = new ProcessStartInfo(CommonHelper.HostExe, testExePath); startInfo.UseShellExecute = false; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = redirectError; startInfo.RedirectStandardInput = redirectInput; + envVars?.ToList().ForEach(item => startInfo.Environment.Add(item.Key, item.Value)); outputHelper = _outputHelper; } @@ -143,7 +146,7 @@ public void PrintStatus() { if (testProcess.HasExited) { - outputHelper.WriteLine($"Process {testProcess.Id} status: Exited {testProcess.ExitCode}"); + outputHelper.WriteLine($"Process {testProcess.Id} status: Exited 0x{testProcess.ExitCode:X}"); } else {