From f0bb8eb89c7b762db010aec3d44ec92a1b232ab3 Mon Sep 17 00:00:00 2001 From: Marc Paine Date: Fri, 30 Aug 2024 16:11:15 -0700 Subject: [PATCH 1/4] Add Dev Device ID --- src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs | 105 ++++++++++++++++++ .../Telemetry/TelemetryCommonProperties.cs | 5 + .../TelemetryCommonPropertiesTests.cs | 16 +++ 3 files changed, 126 insertions(+) create mode 100644 src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs diff --git a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs new file mode 100644 index 000000000000..f4bff2b59fee --- /dev/null +++ b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32; + +namespace Microsoft.DotNet.Cli.Telemetry +{ + internal static class DeviceIdGetter + { + public static string GetDeviceId() + { + string deviceId = GetCachedDeviceId(); + + // Check if the device Id is already cached + if (string.IsNullOrEmpty(deviceId)) + { + // Generate a new guid + deviceId = Guid.NewGuid().ToString("D").ToLowerInvariant(); + + // Cache the new device Id + CacheDeviceId(deviceId); + } + + return deviceId; + } + + private static string GetCachedDeviceId() + { + string deviceId = null; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Get device Id from Windows registry + using (var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) + { + deviceId = key?.GetValue("deviceid") as string; + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + // Get device Id from Linux cache file + string cacheFilePath; + string xdgCacheHome = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); + if (!string.IsNullOrEmpty(xdgCacheHome)) + { + cacheFilePath = Path.Combine(xdgCacheHome, "Microsoft", "DeveloperTools", "deviceid"); + } + else + { + cacheFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache", "deviceid"); + } + + if (File.Exists(cacheFilePath)) + { + deviceId = File.ReadAllText(cacheFilePath); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // Get device Id from macOS cache file + string cacheFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Microsoft", "DeveloperTools", "deviceid"); + if (File.Exists(cacheFilePath)) + { + deviceId = File.ReadAllText(cacheFilePath); + } + } + + return deviceId; + } + + private static void CacheDeviceId(string deviceId) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Cache device Id in Windows registry + using (var key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) + { + key.SetValue("deviceid", deviceId); + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + // Cache device Id in Linux cache file + string cacheFilePath; + string xdgCacheHome = Environment.GetEnvironmentVariable("XDG_CACHE_HOME"); + if (!string.IsNullOrEmpty(xdgCacheHome)) + { + cacheFilePath = Path.Combine(xdgCacheHome, "Microsoft", "DeveloperTools", "deviceId"); + } + else + { + cacheFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache", "deviceid"); + } + + File.WriteAllText(cacheFilePath, deviceId); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // Cache device Id in macOS cache file + string cacheFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Microsoft", "DeveloperTools", "deviceid"); + File.WriteAllText(cacheFilePath, deviceId); + } + } + } +} diff --git a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs index eed36f6149ad..b9081c498869 100644 --- a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs +++ b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs @@ -14,6 +14,7 @@ public TelemetryCommonProperties( Func getCurrentDirectory = null, Func hasher = null, Func getMACAddress = null, + Func getDeviceId = null, IDockerContainerDetector dockerContainerDetector = null, IUserLevelCacheWriter userLevelCacheWriter = null, ICIEnvironmentDetector ciEnvironmentDetector = null) @@ -21,6 +22,7 @@ public TelemetryCommonProperties( _getCurrentDirectory = getCurrentDirectory ?? Directory.GetCurrentDirectory; _hasher = hasher ?? Sha256Hasher.Hash; _getMACAddress = getMACAddress ?? MacAddressGetter.GetMacAddress; + _getDeviceId = getDeviceId ?? DeviceIdGetter.GetDeviceId; _dockerContainerDetector = dockerContainerDetector ?? new DockerContainerDetectorForTelemetry(); _userLevelCacheWriter = userLevelCacheWriter ?? new UserLevelCacheWriter(); _ciEnvironmentDetector = ciEnvironmentDetector ?? new CIEnvironmentDetectorForTelemetry(); @@ -31,6 +33,7 @@ public TelemetryCommonProperties( private Func _getCurrentDirectory; private Func _hasher; private Func _getMACAddress; + private Func _getDeviceId; private IUserLevelCacheWriter _userLevelCacheWriter; private const string OSVersion = "OS Version"; private const string OSPlatform = "OS Platform"; @@ -40,6 +43,7 @@ public TelemetryCommonProperties( private const string ProductVersion = "Product Version"; private const string TelemetryProfile = "Telemetry Profile"; private const string CurrentPathHash = "Current Path Hash"; + private const string DeviceId = "DeviceId"; private const string MachineId = "Machine ID"; private const string MachineIdOld = "Machine ID Old"; private const string DockerContainer = "Docker Container"; @@ -81,6 +85,7 @@ public Dictionary GetTelemetryCommonProperties() CliFolderPathCalculator.DotnetUserProfileFolderPath, $"{MachineIdCacheKey}.v1.dotnetUserLevelCache"), GetMachineId)}, + {DeviceId, _getDeviceId()}, {KernelVersion, GetKernelVersion()}, {InstallationType, ExternalTelemetryProperties.GetInstallationType()}, {ProductType, ExternalTelemetryProperties.GetProductType()}, diff --git a/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs index 36a641beb0c7..8e1d97fba4db 100644 --- a/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -33,6 +33,13 @@ public void TelemetryCommonPropertiesShouldReturnHashedMachineId() unitUnderTest.GetTelemetryCommonProperties()["Machine ID"].Should().NotBe("plaintext"); } + [Fact] + public void TelemetryCommonPropertiesShouldReturnDevDeviceId() + { + var unitUnderTest = new TelemetryCommonProperties(getDeviceId: () => "plaintext", userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties()["DevDeviceId"].Should().Be("plaintext"); + } + [Fact] public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddress() { @@ -42,6 +49,15 @@ public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddress( Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid"); } + [Fact] + public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotDevDeviceId() + { + var unitUnderTest = new TelemetryCommonProperties(getDeviceId: () => null, userLevelCacheWriter: new NothingCache()); + var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties()["DevDeviceId"]; + + Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid"); + } + [Fact] public void TelemetryCommonPropertiesShouldReturnHashedMachineIdOld() { From d2196b04f1ea61f15adda1b1a77ffb0e2fd7ad60 Mon Sep 17 00:00:00 2001 From: Marc Paine Date: Tue, 3 Sep 2024 17:13:58 -0700 Subject: [PATCH 2/4] fix the new deviceid tests Make sure we return an empty string if caching fails but don't error our code refactor the caching code slightly for simplicity --- src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs | 25 ++++++++++++++++--- .../Telemetry/TelemetryCommonProperties.cs | 2 +- .../TelemetryCommonPropertiesTests.cs | 6 ++--- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs index f4bff2b59fee..a4afb8b13933 100644 --- a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs +++ b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs @@ -18,7 +18,15 @@ public static string GetDeviceId() deviceId = Guid.NewGuid().ToString("D").ToLowerInvariant(); // Cache the new device Id - CacheDeviceId(deviceId); + try + { + CacheDeviceId(deviceId); + } + catch + { + // If caching fails, return empty string to avoid sending a non-stored id + deviceId = "" + } } return deviceId; @@ -92,14 +100,25 @@ private static void CacheDeviceId(string deviceId) cacheFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache", "deviceid"); } - File.WriteAllText(cacheFilePath, deviceId); + CreateDirectoryAndWriteToFile(cacheFilePath, deviceId); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { // Cache device Id in macOS cache file string cacheFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Microsoft", "DeveloperTools", "deviceid"); - File.WriteAllText(cacheFilePath, deviceId); + + CreateDirectoryAndWriteToFile(cacheFilePath, deviceId); + } + } + + private static void CreateDirectoryAndWriteToFile(string filePath, string content) + { + string directory = Path.GetDirectoryName(filePath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); } + File.WriteAllText(filePath, content); } } } diff --git a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs index b9081c498869..ce167c4c17f7 100644 --- a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs +++ b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs @@ -43,7 +43,7 @@ public TelemetryCommonProperties( private const string ProductVersion = "Product Version"; private const string TelemetryProfile = "Telemetry Profile"; private const string CurrentPathHash = "Current Path Hash"; - private const string DeviceId = "DeviceId"; + private const string DeviceId = "devdeviceid"; private const string MachineId = "Machine ID"; private const string MachineIdOld = "Machine ID Old"; private const string DockerContainer = "Docker Container"; diff --git a/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs index 8e1d97fba4db..7b1c9af6565e 100644 --- a/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -37,7 +37,7 @@ public void TelemetryCommonPropertiesShouldReturnHashedMachineId() public void TelemetryCommonPropertiesShouldReturnDevDeviceId() { var unitUnderTest = new TelemetryCommonProperties(getDeviceId: () => "plaintext", userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties()["DevDeviceId"].Should().Be("plaintext"); + unitUnderTest.GetTelemetryCommonProperties()["devdeviceid"].Should().Be("plaintext"); } [Fact] @@ -52,8 +52,8 @@ public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddress( [Fact] public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotDevDeviceId() { - var unitUnderTest = new TelemetryCommonProperties(getDeviceId: () => null, userLevelCacheWriter: new NothingCache()); - var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties()["DevDeviceId"]; + var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache()); + var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties()["devdeviceid"]; Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid"); } From 2a52330e6d0d96a7c6f3ba3750aa0f59da6319ca Mon Sep 17 00:00:00 2001 From: Marc Paine Date: Wed, 4 Sep 2024 12:48:25 -0700 Subject: [PATCH 3/4] add trailing ; --- src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs index a4afb8b13933..360e530e3b49 100644 --- a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs +++ b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs @@ -25,7 +25,7 @@ public static string GetDeviceId() catch { // If caching fails, return empty string to avoid sending a non-stored id - deviceId = "" + deviceId = ""; } } From 94341548d9582cd53c0cca50cf25c1dfab79e4b4 Mon Sep 17 00:00:00 2001 From: Marc Paine Date: Wed, 18 Sep 2024 11:39:49 -0700 Subject: [PATCH 4/4] Correctly handle different architectures for devdeviceID (#43471) --- src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs | 16 +++++++++++----- .../TelemetryCommonPropertiesTests.cs | 6 +++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs index 360e530e3b49..2bb88a290745 100644 --- a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs +++ b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs @@ -38,8 +38,8 @@ private static string GetCachedDeviceId() if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - // Get device Id from Windows registry - using (var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) + // Get device Id from Windows registry matching the OS architecture + using (var key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64).OpenSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) { deviceId = key?.GetValue("deviceid") as string; } @@ -80,10 +80,16 @@ private static void CacheDeviceId(string deviceId) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - // Cache device Id in Windows registry - using (var key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) + // Cache device Id in Windows registry matching the OS architecture + using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)) { - key.SetValue("deviceid", deviceId); + using(var key = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) + { + if (key != null) + { + key.SetValue("deviceid", deviceId); + } + } } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) diff --git a/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs index 7b1c9af6565e..0353e76d713c 100644 --- a/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/src/Tests/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -50,12 +50,16 @@ public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotGetMacAddress( } [Fact] - public void TelemetryCommonPropertiesShouldReturnNewGuidWhenCannotDevDeviceId() + public void TelemetryCommonPropertiesShouldEnsureDevDeviceIDIsCached() { var unitUnderTest = new TelemetryCommonProperties(userLevelCacheWriter: new NothingCache()); var assignedMachineId = unitUnderTest.GetTelemetryCommonProperties()["devdeviceid"]; Guid.TryParse(assignedMachineId, out var _).Should().BeTrue("it should be a guid"); + var secondAssignedMachineId = unitUnderTest.GetTelemetryCommonProperties()["devdeviceid"]; + + Guid.TryParse(secondAssignedMachineId, out var _).Should().BeTrue("it should be a guid"); + secondAssignedMachineId.Should().Be(assignedMachineId, "it should match the previously assigned guid"); } [Fact]