diff --git a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs index a309f9182e..b06769e22b 100644 --- a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs +++ b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs @@ -19,7 +19,7 @@ public static IServiceCollection AddWindowsCustomization(this IServiceCollection services.AddSingleton(); services.AddTransient(); - services.AddSingleton(sp => (cacheLocation, environmentVariable) => ActivatorUtilities.CreateInstance(sp, cacheLocation, environmentVariable)); + services.AddSingleton(sp => (cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters) => ActivatorUtilities.CreateInstance(sp, cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters)); services.AddSingleton(); services.AddTransient(); diff --git a/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs b/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs index 8fb1762a18..8b9223c3cc 100644 --- a/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs +++ b/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs @@ -17,5 +17,5 @@ public partial class DevDriveCacheData public List? CacheDirectory { get; set; } - public string? ExampleDirectory { get; set; } + public string? ExampleSubDirectory { get; set; } } diff --git a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw index d6a6c7a6f7..3914c8eb00 100644 --- a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw +++ b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw @@ -126,7 +126,7 @@ Dev drive size free - All things, dev drives, optimizations, etc. + All things, Dev Drives, optimizations, etc. The description for the Dev Drive Insights settings card @@ -161,9 +161,9 @@ Enable end task in taskbar by right click The description for the end task on task bar settings card - - Example: E:\packages\pip - Example dev drive location + + Example: + Example string, will be followed by a sample location to move the cache to a dev drive location End Task diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs index aa64ba3f0b..e0175b81de 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -18,6 +19,8 @@ public partial class DevDriveOptimizerCardViewModel : ObservableObject { public OptimizeDevDriveDialogViewModelFactory OptimizeDevDriveDialogViewModelFactory { get; set; } + public List ExistingDevDriveLetters { get; set; } + public string CacheToBeMoved { get; set; } public string DevDriveOptimizationSuggestion { get; set; } @@ -41,7 +44,11 @@ private async Task OptimizeDevDriveAsync(object sender) var settingsCard = sender as Button; if (settingsCard != null) { - var optimizeDevDriveViewModel = OptimizeDevDriveDialogViewModelFactory(ExistingCacheLocation, EnvironmentVariableToBeSet); + var optimizeDevDriveViewModel = OptimizeDevDriveDialogViewModelFactory( + ExistingCacheLocation, + EnvironmentVariableToBeSet, + ExampleLocationOnDevDrive, + ExistingDevDriveLetters); var optimizeDevDriveDialog = new OptimizeDevDriveDialog(optimizeDevDriveViewModel); optimizeDevDriveDialog.XamlRoot = settingsCard.XamlRoot; optimizeDevDriveDialog.RequestedTheme = settingsCard.ActualTheme; @@ -49,9 +56,16 @@ private async Task OptimizeDevDriveAsync(object sender) } } - public DevDriveOptimizerCardViewModel(OptimizeDevDriveDialogViewModelFactory optimizeDevDriveDialogViewModelFactory, string cacheToBeMoved, string existingCacheLocation, string exampleLocationOnDevDrive, string environmentVariableToBeSet) + public DevDriveOptimizerCardViewModel( + OptimizeDevDriveDialogViewModelFactory optimizeDevDriveDialogViewModelFactory, + string cacheToBeMoved, + string existingCacheLocation, + string exampleLocationOnDevDrive, + string environmentVariableToBeSet, + List existingDevDriveLetters) { OptimizeDevDriveDialogViewModelFactory = optimizeDevDriveDialogViewModelFactory; + ExistingDevDriveLetters = existingDevDriveLetters; CacheToBeMoved = cacheToBeMoved; ExistingCacheLocation = existingCacheLocation; ExampleLocationOnDevDrive = exampleLocationOnDevDrive; diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs index 46c5c7c000..340da507e9 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs @@ -2,15 +2,22 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Data.SqlTypes; using System.IO; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; using DevHome.Common.Services; +using DevHome.Common.TelemetryEvents; +using DevHome.Telemetry; +using Microsoft.UI.Xaml.Controls; using Serilog; +using Windows.Media.Protection; using Windows.Storage.Pickers; using WinUIEx; +using static Microsoft.Extensions.Logging.EventSource.LoggingEventSource; namespace DevHome.Customization.ViewModels.DevDriveInsights; @@ -19,6 +26,9 @@ namespace DevHome.Customization.ViewModels.DevDriveInsights; /// public partial class OptimizeDevDriveDialogViewModel : ObservableObject { + [ObservableProperty] + private List _existingDevDriveLetters; + [ObservableProperty] private string _exampleDevDriveLocation; @@ -40,11 +50,16 @@ public partial class OptimizeDevDriveDialogViewModel : ObservableObject [ObservableProperty] private string _directoryPathTextBox; - public OptimizeDevDriveDialogViewModel(string existingCacheLocation, string environmentVariableToBeSet) + public OptimizeDevDriveDialogViewModel( + string existingCacheLocation, + string environmentVariableToBeSet, + string exampleDevDriveLocation, + List existingDevDriveLetters) { DirectoryPathTextBox = string.Empty; var stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); - ExampleDevDriveLocation = stringResource.GetLocalized("ExampleDevDriveLocation"); + ExistingDevDriveLetters = existingDevDriveLetters; + ExampleDevDriveLocation = stringResource.GetLocalized("ExampleText") + exampleDevDriveLocation; ChooseDirectoryPromptText = stringResource.GetLocalized("ChooseDirectoryPromptText"); MakeChangesText = stringResource.GetLocalized("MakeChangesText"); ExistingCacheLocation = existingCacheLocation; @@ -78,7 +93,20 @@ private async Task BrowseButtonClick(object sender) } } - private void MoveDirectory(string sourceDirectory, string targetDirectory) + private string RemovePrivacyInfo(string input) + { + var output = input; + var userProfilePath = Environment.ExpandEnvironmentVariables("%userprofile%"); + if (input.StartsWith(userProfilePath, StringComparison.OrdinalIgnoreCase)) + { + var index = input.LastIndexOf(userProfilePath, StringComparison.OrdinalIgnoreCase) + userProfilePath.Length; + output = Path.Join("%userprofile%", input.Substring(index)); + } + + return output; + } + + private bool MoveDirectory(string sourceDirectory, string targetDirectory) { try { @@ -110,10 +138,13 @@ private void MoveDirectory(string sourceDirectory, string targetDirectory) // Delete the source directory Directory.Delete(sourceDirectory, true); + return true; } catch (Exception ex) { - Log.Error(ex, $"Error in MoveDirectory. Error: {ex}"); + Log.Error($"Error in MoveDirectory. Error: {ex}"); + TelemetryFactory.Get().LogError("DevDriveInsights_PackageCacheMoveDirectory_Error", LogLevel.Critical, new ExceptionEvent(ex.HResult, RemovePrivacyInfo(sourceDirectory))); + return false; } } @@ -129,6 +160,19 @@ private void SetEnvironmentVariable(string variableName, string value) } } + private bool ChosenDirectoryInDevDrive(string directoryPath) + { + foreach (var devDriveLetter in ExistingDevDriveLetters) + { + if (directoryPath.StartsWith(devDriveLetter + ":", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + [RelayCommand] private void DirectoryInputConfirmed() { @@ -137,9 +181,21 @@ private void DirectoryInputConfirmed() if (!string.IsNullOrEmpty(directoryPath)) { // Handle the selected folder - // TODO: If chosen folder not a dev drive location, currently we no-op. Instead we should display the error. - MoveDirectory(ExistingCacheLocation, directoryPath); - SetEnvironmentVariable(EnvironmentVariableToBeSet, directoryPath); + // TODO: If chosen folder not a dev drive location, currently we no-op and log the error. Instead we should display the error. + if (ChosenDirectoryInDevDrive(directoryPath)) + { + if (MoveDirectory(ExistingCacheLocation, directoryPath)) + { + SetEnvironmentVariable(EnvironmentVariableToBeSet, directoryPath); + var existingCacheLocationVetted = RemovePrivacyInfo(ExistingCacheLocation); + Log.Debug($"Moved cache from {existingCacheLocationVetted} to {directoryPath}"); + TelemetryFactory.Get().Log("DevDriveInsights_PackageCacheMovedSuccessfully_Event", LogLevel.Critical, new ExceptionEvent(0, existingCacheLocationVetted)); + } + } + else + { + Log.Error($"Chosen directory {directoryPath} not on a dev drive."); + } } } } diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs index 368eb7c1c1..15000d4f2c 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs @@ -12,12 +12,17 @@ using DevHome.Customization.Helpers; using DevHome.Customization.ViewModels.DevDriveInsights; using DevHome.Customization.Views; +using Microsoft.Internal.Windows.DevHome.Helpers; using Serilog; namespace DevHome.Customization.ViewModels; public partial class DevDriveInsightsViewModel : ObservableObject { + private readonly ShellSettings _shellSettings; + + public ObservableCollection Breadcrumbs { get; } + public ObservableCollection DevDriveCardCollection { get; private set; } = new(); public ObservableCollection DevDriveOptimizerCardCollection { get; private set; } = new(); @@ -48,12 +53,29 @@ public partial class DevDriveInsightsViewModel : ObservableObject private IEnumerable ExistingDevDrives { get; set; } = Enumerable.Empty(); + private static readonly string _appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + private static readonly string _localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); private static readonly string _userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + private const string PackagesStr = "packages"; + + private const string CacheStr = "cache"; + + private const string ArchivesStr = "archives"; + public DevDriveInsightsViewModel(IDevDriveManager devDriveManager, OptimizeDevDriveDialogViewModelFactory optimizeDevDriveDialogViewModelFactory) { + _shellSettings = new ShellSettings(); + + var stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); + Breadcrumbs = + [ + new(stringResource.GetLocalized("MainPage_Header"), typeof(MainPageViewModel).FullName!), + new(stringResource.GetLocalized("DevDriveInsights_Header"), typeof(DevDriveInsightsViewModel).FullName!) + ]; + _optimizeDevDriveDialogViewModelFactory = optimizeDevDriveDialogViewModelFactory; DevDriveManagerObj = devDriveManager; } @@ -237,17 +259,60 @@ public void UpdateListViewModelList() EnvironmentVariable = "PIP_CACHE_DIR", CacheDirectory = new List { - Path.Join(_localAppDataPath, "pip", "cache"), - Path.Join(_localAppDataPath, "packages", "PythonSoftwareFoundation.Python"), + Path.Join(_localAppDataPath, "pip", CacheStr), + Path.Join(_localAppDataPath, PackagesStr, "PythonSoftwareFoundation.Python"), }, - ExampleDirectory = Path.Join("D:", "packages", "pip", "cache"), + ExampleSubDirectory = Path.Join(PackagesStr, "pip", CacheStr), }, new DevDriveCacheData { CacheName = "NuGet cache (dotnet)", EnvironmentVariable = "NUGET_PACKAGES", - CacheDirectory = new List { Path.Join(_userProfilePath, ".nuget", "packages") }, - ExampleDirectory = Path.Join("D:", "packages", "NuGet", "Cache"), + CacheDirectory = new List { Path.Join(_userProfilePath, ".nuget", PackagesStr) }, + ExampleSubDirectory = Path.Join(PackagesStr, "NuGet", CacheStr), + }, + new DevDriveCacheData + { + CacheName = "Npm cache (NodeJS)", + EnvironmentVariable = "NPM_CONFIG_CACHE", + CacheDirectory = new List + { + Path.Join(_appDataPath, "npm-cache"), + Path.Join(_localAppDataPath, "npm-cache"), + }, + ExampleSubDirectory = Path.Join(PackagesStr, "npm"), + }, + new DevDriveCacheData + { + CacheName = "Vcpkg cache", + EnvironmentVariable = "VCPKG_DEFAULT_BINARY_CACHE", + CacheDirectory = new List + { + Path.Join(_appDataPath, "vcpkg", ArchivesStr), + Path.Join(_localAppDataPath, "vcpkg", ArchivesStr), + }, + ExampleSubDirectory = Path.Join(PackagesStr, "vcpkg"), + }, + new DevDriveCacheData + { + CacheName = "Cargo cache (Rust)", + EnvironmentVariable = "CARGO_HOME", + CacheDirectory = new List { Path.Join(_userProfilePath, ".cargo") }, + ExampleSubDirectory = Path.Join(PackagesStr, "cargo"), + }, + new DevDriveCacheData + { + CacheName = "Maven cache (Java)", + EnvironmentVariable = "MAVEN_OPTS", + CacheDirectory = new List { Path.Join(_userProfilePath, ".m2") }, + ExampleSubDirectory = Path.Join(PackagesStr, "m2"), + }, + new DevDriveCacheData + { + CacheName = "Gradle cache (Java)", + EnvironmentVariable = "GRADLE_USER_HOME", + CacheDirectory = new List { Path.Join(_userProfilePath, ".gradle") }, + ExampleSubDirectory = Path.Join(PackagesStr, "gradle"), } ]; @@ -261,13 +326,13 @@ public void UpdateListViewModelList() } else { - var subDirectories = Directory.GetDirectories(_localAppDataPath + "\\Packages", "*", SearchOption.TopDirectoryOnly); + var subDirectories = Directory.GetDirectories(Path.Join(_localAppDataPath, PackagesStr), "*", SearchOption.TopDirectoryOnly); var matchingSubdirectory = subDirectories.FirstOrDefault(subdir => subdir.StartsWith(cacheDirectory, StringComparison.OrdinalIgnoreCase)); if (Directory.Exists(matchingSubdirectory)) { if (matchingSubdirectory.Contains("PythonSoftwareFoundation")) { - return Path.Join(matchingSubdirectory, "LocalCache", "Local", "pip", "cache"); + return Path.Join(matchingSubdirectory, "LocalCache", "Local", "pip", CacheStr); } return matchingSubdirectory; @@ -307,12 +372,16 @@ public void UpdateOptimizerListViewModelList() continue; } + List existingDevDriveLetters = ExistingDevDrives.Select(x => x.DriveLetter.ToString()).ToList(); + + var exampleDirectory = Path.Join(existingDevDriveLetters[0] + ":", cache.ExampleSubDirectory); var card = new DevDriveOptimizerCardViewModel( _optimizeDevDriveDialogViewModelFactory, cache.CacheName!, existingCacheLocation, - cache.ExampleDirectory!, // example location on dev drive to move cache to - cache.EnvironmentVariable!); // environmentVariableToBeSet + exampleDirectory!, // example location on dev drive to move cache to + cache.EnvironmentVariable!, // environmentVariableToBeSet + existingDevDriveLetters); DevDriveOptimizerCardCollection.Add(card); } diff --git a/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs index cff6dc052c..b54ea197af 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs @@ -3,12 +3,14 @@ using System; using System.Collections.ObjectModel; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; using DevHome.Common.Models; using DevHome.Common.Services; +using Microsoft.UI.Xaml; using Windows.System; namespace DevHome.Customization.ViewModels; @@ -45,4 +47,6 @@ private void NavigateToDevDriveInsightsPage() { NavigationService.NavigateTo(typeof(DevDriveInsightsViewModel).FullName!); } + + public bool AnyDevDrivesPresent => Application.Current.GetService().GetAllDevDrivesThatExistOnSystem().Any(); } diff --git a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml index abce444f88..e9ad085133 100644 --- a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml +++ b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml @@ -1,22 +1,15 @@ - - - - - - - - - - - - - + + + + + + + + diff --git a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml index 64ad93ec49..c7b8080aae 100644 --- a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml +++ b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml @@ -22,6 +22,7 @@ AutomationProperties.AccessibilityView="Control" AutomationProperties.AutomationId="NavigateDevDriveInsightsCardButton" Command="{x:Bind ViewModel.NavigateToDevDriveInsightsPageCommand}" + Visibility="{x:Bind ViewModel.AnyDevDrivesPresent}" IsClickEnabled="True" > diff --git a/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs b/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs index 8191dc757f..b6cde57e4b 100644 --- a/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs +++ b/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs @@ -1,12 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; using DevHome.Customization.ViewModels.DevDriveInsights; using Microsoft.UI.Xaml.Controls; namespace DevHome.Customization.Views; -public delegate OptimizeDevDriveDialogViewModel OptimizeDevDriveDialogViewModelFactory(string existingCacheLocation, string environmentVariableToBeSet); +public delegate OptimizeDevDriveDialogViewModel OptimizeDevDriveDialogViewModelFactory( + string existingCacheLocation, + string environmentVariableToBeSet, + string exampleDevDriveLocation, + List existingDevDriveLetters); public sealed partial class OptimizeDevDriveDialog : ContentDialog {