From f9c40ce94d7ced870ad48026d35022d56acbb6f4 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Tue, 27 Feb 2018 18:55:35 -0800 Subject: [PATCH] Log Windows product type and installation type, and Linux libc version (#8688) * Installation type * Product Type * Libc Release and Version * Catch all * Fix test * Fix mac test * Extract class * Remove CharSet * Remove extraneous assignment * Missing space * Typo * Fix comment XML * CR feedback --- .../Telemetry/ExternalTelemetryProperties.cs | 134 ++++++++++++++++++ .../Telemetry/TelemetryCommonProperties.cs | 14 +- .../UnixOnlyFactAttribute.cs | 19 +++ .../TelemetryCommonPropertiesTests.cs | 55 +++++++ 4 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 src/dotnet/Telemetry/ExternalTelemetryProperties.cs create mode 100644 test/Microsoft.DotNet.Tools.Tests.Utilities/UnixOnlyFactAttribute.cs diff --git a/src/dotnet/Telemetry/ExternalTelemetryProperties.cs b/src/dotnet/Telemetry/ExternalTelemetryProperties.cs new file mode 100644 index 000000000..42a2ad9fc --- /dev/null +++ b/src/dotnet/Telemetry/ExternalTelemetryProperties.cs @@ -0,0 +1,134 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security; +using Microsoft.DotNet.PlatformAbstractions; +using Microsoft.Win32; +using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; +using System.Runtime.InteropServices; +using System.Diagnostics; + +namespace Microsoft.DotNet.Cli.Telemetry +{ + // Some properties we need for telemetry, that don't yet have suitable + // public API + internal static class ExternalTelemetryProperties + { + /// + /// For Windows, returns the OS installation type, eg. "Nano Server", "Server Core", "Server", or "Client". + /// For Unix, or on error, currently returns empty string. + /// + internal static string GetInstallationType() + { + if (RuntimeEnvironment.OperatingSystemPlatform != Platform.Windows) + { + return ""; + } + + const string Key = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion"; + const string ValueName = @"InstallationType"; + + try + { + return (string)Registry.GetValue(Key, ValueName, defaultValue: ""); + } + // Catch everything: this is for telemetry only. + catch (Exception e) + { + Debug.Assert(e is ArgumentException | e is SecurityException | e is InvalidCastException); + return ""; + } + } + + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = false)] + private static extern bool GetProductInfo(uint dwOSMajorVersion, uint dwOSMinorVersion, uint dwSpMajorVersion, uint dwSpMinorVersion, out uint pdwReturnedProductType); + + /// + /// For Windows, returns the product type, loosely the SKU, as encoded by GetProductInfo(). + /// For example, Enterprise is "4" (0x4) and Professional is "48" (0x30) + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms724358(v=vs.85).aspx for the full list. + /// We're not attempting to decode the value on the client side as new Windows releases may add new values. + /// For Unix, or on error, returns an empty string. + /// + internal static string GetProductType() + { + if (RuntimeEnvironment.OperatingSystemPlatform != Platform.Windows) + { + return ""; + } + + try + { + if (GetProductInfo((uint)Environment.OSVersion.Version.Major, (uint)Environment.OSVersion.Version.Minor, 0, 0, out uint productType)) + { + return productType.ToString("D", CultureInfo.InvariantCulture); + } + } + // Catch everything: this is for telemetry only + catch (Exception e) + { + Debug.Assert(false, $"Unexpected exception from GetProductInfo: ${e.GetType().Name}: ${e.Message}"); + } + + return ""; + } + + [DllImport("libc", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr gnu_get_libc_release(); + + [DllImport("libc", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr gnu_get_libc_version(); + + /// + /// If gnulibc is available, returns the release, such as "stable". + /// If the libc is musl, currently returns empty string. + /// Otherwise returns empty string. + /// + internal static string GetLibcRelease() + { + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + return ""; + } + + try + { + return Marshal.PtrToStringUTF8(gnu_get_libc_release()); + } + // Catch everything: this is for telemetry only + catch (Exception e) + { + Debug.Assert(e is DllNotFoundException || e is EntryPointNotFoundException); + return ""; + } + } + + /// + /// If gnulibc is available, returns the version, such as "2.22". + /// If the libc is musl, currently returns empty string. (In future could run "ldd -version".) + /// Otherwise returns empty string. + /// + internal static string GetLibcVersion() + { + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + return ""; + } + + try + { + return Marshal.PtrToStringUTF8(gnu_get_libc_version()); + } + // Catch everything: this is for telemetry only + catch (Exception e) + { + Debug.Assert(e is DllNotFoundException || e is EntryPointNotFoundException); + return ""; + } + } + } +} \ No newline at end of file diff --git a/src/dotnet/Telemetry/TelemetryCommonProperties.cs b/src/dotnet/Telemetry/TelemetryCommonProperties.cs index 6cbf02b47..a37cd70ef 100644 --- a/src/dotnet/Telemetry/TelemetryCommonProperties.cs +++ b/src/dotnet/Telemetry/TelemetryCommonProperties.cs @@ -6,10 +6,14 @@ using System.Collections.Generic; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.PlatformAbstractions; using System.IO; +using System.Security; using Microsoft.DotNet.Configurer; +using Microsoft.Win32; using System.Linq; using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; using RuntimeInformation = System.Runtime.InteropServices.RuntimeInformation; +using System.Runtime.InteropServices; +using System.Diagnostics; namespace Microsoft.DotNet.Cli.Telemetry { @@ -43,6 +47,10 @@ namespace Microsoft.DotNet.Cli.Telemetry private const string MachineId = "Machine ID"; private const string DockerContainer = "Docker Container"; private const string KernelVersion = "Kernel Version"; + private const string InstallationType = "Installation Type"; + private const string ProductType = "Product Type"; + private const string LibcRelease = "Libc Release"; + private const string LibcVersion = "Libc Version"; private const string TelemetryProfileEnvironmentVariable = "DOTNET_CLI_TELEMETRY_PROFILE"; private const string CannotFindMacAddress = "Unknown"; @@ -62,7 +70,11 @@ namespace Microsoft.DotNet.Cli.Telemetry {DockerContainer, _userLevelCacheWriter.RunWithCache(IsDockerContainerCacheKey, () => _dockerContainerDetector.IsDockerContainer().ToString("G") )}, {CurrentPathHash, _hasher(_getCurrentDirectory())}, {MachineId, _userLevelCacheWriter.RunWithCache(MachineIdCacheKey, GetMachineId)}, - {KernelVersion, GetKernelVersion()} + {KernelVersion, GetKernelVersion()}, + {InstallationType, ExternalTelemetryProperties.GetInstallationType()}, + {ProductType, ExternalTelemetryProperties.GetProductType()}, + {LibcRelease, ExternalTelemetryProperties.GetLibcRelease()}, + {LibcVersion, ExternalTelemetryProperties.GetLibcVersion()} }; } diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/UnixOnlyFactAttribute.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/UnixOnlyFactAttribute.cs new file mode 100644 index 000000000..78be9c856 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/UnixOnlyFactAttribute.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.PlatformAbstractions; +using Xunit; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public class UnixOnlyFactAttribute : FactAttribute + { + public UnixOnlyFactAttribute() + { + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + this.Skip = "This test requires Unix to run"; + } + } + } +} diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs index 7c6c5484d..6404a2b58 100644 --- a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -52,6 +52,61 @@ namespace Microsoft.DotNet.Tests unitUnderTest.GetTelemetryCommonProperties()["Kernel Version"].Should().Be(RuntimeInformation.OSDescription); } + [WindowsOnlyFact] + public void TelemetryCommonPropertiesShouldContainWindowsInstallType() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties()["Installation Type"].Should().NotBeEmpty(); + } + + [UnixOnlyFact] + public void TelemetryCommonPropertiesShouldContainEmptyWindowsInstallType() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties()["Installation Type"].Should().BeEmpty(); + } + + [WindowsOnlyFact] + public void TelemetryCommonPropertiesShouldContainWindowsProductType() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties()["Product Type"].Should().NotBeEmpty(); + } + + [UnixOnlyFact] + public void TelemetryCommonPropertiesShouldContainEmptyWindowsProductType() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties()["Product Type"].Should().BeEmpty(); + } + + [WindowsOnlyFact] + public void TelemetryCommonPropertiesShouldContainEmptyLibcReleaseAndVersion() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties()["Libc Release"].Should().BeEmpty(); + unitUnderTest.GetTelemetryCommonProperties()["Libc Version"].Should().BeEmpty(); + } + + [MacOsOnlyFact] + public void TelemetryCommonPropertiesShouldContainEmptyLibcReleaseAndVersion2() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties()["Libc Release"].Should().BeEmpty(); + unitUnderTest.GetTelemetryCommonProperties()["Libc Version"].Should().BeEmpty(); + } + + [LinuxOnlyFact] + public void TelemetryCommonPropertiesShouldContainLibcReleaseAndVersion() + { + if (!RuntimeEnvironment.OperatingSystem.Contains("Alpine", StringComparison.OrdinalIgnoreCase)) + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties()["Libc Release"].Should().NotBeEmpty(); + unitUnderTest.GetTelemetryCommonProperties()["Libc Version"].Should().NotBeEmpty(); + } + } + private class NothingCache : IUserLevelCacheWriter { public string RunWithCache(string cacheKey, Func getValueToCache)