diff --git a/common/Helpers/ManagementInfrastructureHelper.cs b/common/Helpers/ManagementInfrastructureHelper.cs index 6342205158..29645694ba 100644 --- a/common/Helpers/ManagementInfrastructureHelper.cs +++ b/common/Helpers/ManagementInfrastructureHelper.cs @@ -4,6 +4,7 @@ using System; using Microsoft.Management.Infrastructure; using Serilog; +using static DevHome.Common.Helpers.WindowsOptionalFeatures; namespace DevHome.Common.Helpers; @@ -22,6 +23,11 @@ public static class ManagementInfrastructureHelper private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(ManagementInfrastructureHelper)); public static FeatureAvailabilityKind IsWindowsFeatureAvailable(string featureName) + { + return GetWindowsFeatureDetails(featureName)?.AvailabilityKind ?? FeatureAvailabilityKind.Unknown; + } + + public static FeatureInfo? GetWindowsFeatureDetails(string featureName) { try { @@ -36,7 +42,19 @@ public static FeatureAvailabilityKind IsWindowsFeatureAvailable(string featureNa var featureAvailability = GetAvailabilityKindFromState(installState); _log.Information($"Found feature: '{featureName}' with enablement state: '{featureAvailability}'"); - return featureAvailability; + + // Most optional features do not have a description, so we provide one for known features + var description = featureInstance.CimInstanceProperties["Description"]?.Value as string; + if (string.IsNullOrEmpty(description) && WindowsOptionalFeatures.FeatureDescriptions.TryGetValue(featureName, out var featureDescription)) + { + description = featureDescription; + } + + return new FeatureInfo( + featureName, + featureInstance.CimInstanceProperties["Caption"]?.Value as string ?? featureName, + description ?? string.Empty, + featureAvailability); } } } @@ -46,7 +64,7 @@ public static FeatureAvailabilityKind IsWindowsFeatureAvailable(string featureNa } _log.Information($"Unable to get state of {featureName} feature"); - return FeatureAvailabilityKind.Unknown; + return null; } private static FeatureAvailabilityKind GetAvailabilityKindFromState(uint state) diff --git a/common/Helpers/RestartHelper.cs b/common/Helpers/RestartHelper.cs new file mode 100644 index 0000000000..2064cb7fab --- /dev/null +++ b/common/Helpers/RestartHelper.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using CommunityToolkit.Mvvm.Input; + +namespace DevHome.Common.Helpers; + +public partial class RestartHelper +{ + [RelayCommand] + public static void RestartComputer() + { + var startInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + FileName = Environment.SystemDirectory + "\\shutdown.exe", + Arguments = "-r -t 0", + Verb = string.Empty, + }; + + var process = new Process + { + StartInfo = startInfo, + }; + process.Start(); + } +} diff --git a/common/Helpers/WindowsIdentityHelper.cs b/common/Helpers/WindowsIdentityHelper.cs index 1c6a76b646..af69300681 100644 --- a/common/Helpers/WindowsIdentityHelper.cs +++ b/common/Helpers/WindowsIdentityHelper.cs @@ -26,4 +26,14 @@ public virtual bool IsUserHyperVAdmin() { return _currentUserIdentity?.Name; } + + // Returns true if the current user has the built-in Administrators claim indicating that + // they could elevate to an administrator role via UAC if needed. Does not check if the process + // is running elevated. + public static bool IsUserAdministrator() + { + using WindowsIdentity identity = WindowsIdentity.GetCurrent(); + var adminSid = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Value; + return identity.Claims.Any(c => c.Value == adminSid); + } } diff --git a/common/Helpers/WindowsOptionalFeatures.cs b/common/Helpers/WindowsOptionalFeatures.cs new file mode 100644 index 0000000000..513456b11c --- /dev/null +++ b/common/Helpers/WindowsOptionalFeatures.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using DevHome.Common.Environments.Helpers; + +namespace DevHome.Common.Helpers; + +public static class WindowsOptionalFeatures +{ + public const string Containers = "Containers"; + public const string GuardedHost = "HostGuardian"; + public const string HyperV = "Microsoft-Hyper-V-All"; + public const string HyperVManagementTools = "Microsoft-Hyper-V-Tools-All"; + public const string HyperVPlatform = "Microsoft-Hyper-V"; + public const string VirtualMachinePlatform = "VirtualMachinePlatform"; + public const string WindowsHypervisorPlatform = "HypervisorPlatform"; + public const string WindowsSandbox = "Containers-DisposableClientVM"; + public const string WindowsSubsystemForLinux = "Microsoft-Windows-Subsystem-Linux"; + + public static IEnumerable VirtualMachineFeatures => new[] + { + Containers, + GuardedHost, + HyperV, + HyperVManagementTools, + HyperVPlatform, + VirtualMachinePlatform, + WindowsHypervisorPlatform, + WindowsSandbox, + WindowsSubsystemForLinux, + }; + + public static readonly Dictionary FeatureDescriptions = new() + { + { Containers, GetFeatureDescription(nameof(Containers)) }, + { GuardedHost, GetFeatureDescription(nameof(GuardedHost)) }, + { HyperV, GetFeatureDescription(nameof(HyperV)) }, + { HyperVManagementTools, GetFeatureDescription(nameof(HyperVManagementTools)) }, + { HyperVPlatform, GetFeatureDescription(nameof(HyperVPlatform)) }, + { VirtualMachinePlatform, GetFeatureDescription(nameof(VirtualMachinePlatform)) }, + { WindowsHypervisorPlatform, GetFeatureDescription(nameof(WindowsHypervisorPlatform)) }, + { WindowsSandbox, GetFeatureDescription(nameof(WindowsSandbox)) }, + { WindowsSubsystemForLinux, GetFeatureDescription(nameof(WindowsSubsystemForLinux)) }, + }; + + private static string GetFeatureDescription(string featureName) + { + return StringResourceHelper.GetResource(featureName + "Description"); + } + + public class FeatureInfo + { + public string FeatureName { get; set; } + + public string DisplayName { get; set; } + + public string Description { get; set; } + + public bool IsEnabled { get; set; } + + public bool IsAvailable { get; set; } + + public FeatureAvailabilityKind AvailabilityKind { get; set; } + + public FeatureInfo(string featureName, string displayName, string description, FeatureAvailabilityKind availabilityKind) + { + FeatureName = featureName; + DisplayName = displayName; + Description = description; + AvailabilityKind = availabilityKind; + IsEnabled = AvailabilityKind == FeatureAvailabilityKind.Enabled; + IsAvailable = AvailabilityKind == FeatureAvailabilityKind.Enabled || AvailabilityKind == FeatureAvailabilityKind.Disabled; + } + } +} diff --git a/common/Models/WindowsOptionalFeatureState.cs b/common/Models/WindowsOptionalFeatureState.cs new file mode 100644 index 0000000000..b807e8b042 --- /dev/null +++ b/common/Models/WindowsOptionalFeatureState.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.ComponentModel; +using static DevHome.Common.Helpers.WindowsOptionalFeatures; + +namespace DevHome.Common.Models; + +public partial class WindowsOptionalFeatureState : ObservableObject +{ + public FeatureInfo Feature { get; } + + [ObservableProperty] + private bool _isModifiable; + + private bool _isEnabled; + + public bool IsEnabled + { + get => _isEnabled; + set + { + if (SetProperty(ref _isEnabled, value)) + { + OnPropertyChanged(nameof(HasChanged)); + } + } + } + + public bool HasChanged => IsEnabled != Feature.IsEnabled; + + public WindowsOptionalFeatureState(FeatureInfo feature, bool modifiable) + { + Feature = feature; + IsEnabled = feature.IsEnabled; + IsModifiable = modifiable; + } +} diff --git a/common/Scripts/ModifyLongPathsSetting.cs b/common/Scripts/ModifyLongPathsSetting.cs new file mode 100644 index 0000000000..8f0b7a3fb3 --- /dev/null +++ b/common/Scripts/ModifyLongPathsSetting.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using Serilog; + +namespace DevHome.Common.Scripts; + +public static class ModifyLongPathsSetting +{ + public static ExitCode ModifyLongPaths(bool enabled, ILogger? log = null) + { + var scriptString = enabled ? EnableScript : DisableScript; + var process = new Process + { + StartInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + FileName = "powershell.exe", + Arguments = $"-ExecutionPolicy Bypass -Command {scriptString}", + UseShellExecute = true, + Verb = "runas", + }, + }; + + try + { + process.Start(); + process.WaitForExit(); + + return FromExitCode(process.ExitCode); + } + catch (Exception ex) + { + log?.Error(ex, "Failed to modify Long Paths setting"); + return ExitCode.Failure; + } + } + + public enum ExitCode + { + Success = 0, + Failure = 1, + } + + private static ExitCode FromExitCode(int exitCode) + { + return exitCode switch + { + 0 => ExitCode.Success, + _ => ExitCode.Failure, + }; + } + + private const string EnableScript = +@" +Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 +if ($?) { exit 0 } else { exit 1 } +"; + + private const string DisableScript = +@" +Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 0 +if ($?) { exit 0 } else { exit 1 } +"; +} diff --git a/common/Scripts/ModifyWindowsOptionalFeatures.cs b/common/Scripts/ModifyWindowsOptionalFeatures.cs new file mode 100644 index 0000000000..872ee11107 --- /dev/null +++ b/common/Scripts/ModifyWindowsOptionalFeatures.cs @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DevHome.Common.Models; +using DevHome.Common.TelemetryEvents; +using Serilog; + +namespace DevHome.Common.Scripts; + +public static class ModifyWindowsOptionalFeatures +{ + public static async Task ModifyFeaturesAsync( + IEnumerable features, + ILogger? log = null, + CancellationToken cancellationToken = default) + { + if (!features.Any(f => f.HasChanged)) + { + return ExitCode.NoChange; + } + + // Format the argument for the PowerShell script using `n as a newline character since the list + // will be parsed with ConvertFrom-StringData. + // The format is FeatureName1=True|False`nFeatureName2=True|False`n... + var featuresString = string.Empty; + foreach (var featureState in features) + { + if (featureState.HasChanged) + { + featuresString += $"{featureState.Feature.FeatureName}={featureState.IsEnabled}`n"; + } + } + + var scriptString = Script.Replace("FEATURE_STRING_INPUT", featuresString); + var process = new Process + { + StartInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + FileName = "powershell.exe", + Arguments = $"-ExecutionPolicy Bypass -Command {scriptString}", + UseShellExecute = true, + Verb = "runas", + }, + }; + + var exitCode = ExitCode.Failure; + + Stopwatch stopwatch = Stopwatch.StartNew(); + + await Task.Run( + () => + { + // Since a UAC prompt will be shown, we need to wait for the process to exit + // This can also be cancelled by the user which will result in an exception, + // which is handled as a failure. + try + { + if (cancellationToken.IsCancellationRequested) + { + log?.Information("Operation was cancelled."); + exitCode = ExitCode.Cancelled; + return; + } + + process.Start(); + while (!process.WaitForExit(1000)) + { + if (cancellationToken.IsCancellationRequested) + { + // Attempt to kill the process if cancellation is requested + exitCode = ExitCode.Cancelled; + process.Kill(); + log?.Information("Operation was cancelled."); + return; + } + } + + exitCode = FromExitCode(process.ExitCode); + } + catch (Exception ex) + { + // This is most likely a case where the user cancelled the UAC prompt. + if (ex is System.ComponentModel.Win32Exception win32Exception) + { + if (win32Exception.NativeErrorCode == 1223) + { + log?.Information(ex, "UAC was cancelled by the user."); + exitCode = ExitCode.Cancelled; + } + } + else + { + log?.Error(ex, "Script failed"); + exitCode = ExitCode.Failure; + } + } + }, + cancellationToken); + + stopwatch.Stop(); + + ModifyWindowsOptionalFeaturesEvent.Log( + featuresString, + exitCode, + stopwatch.ElapsedMilliseconds); + + return exitCode; + } + + public enum ExitCode + { + Success = 0, + NoChange = 1, + Failure = 2, + Cancelled = 3, + } + + private static ExitCode FromExitCode(int exitCode) + { + return exitCode switch + { + 0 => ExitCode.Success, + 1 => ExitCode.NoChange, + 2 => ExitCode.Failure, + _ => ExitCode.Cancelled, + }; + } + + /// + /// PowerShell script for modifying Windows optional features. + /// + /// This script takes a string argument representing feature names and their desired states (enabled or disabled). + /// It parses this string into a dictionary, iterates over each feature, and performs the necessary enable or disable operation based on the desired state. + /// + /// The script defines the following possible exit statuses: + /// - OperationSucceeded (0): All operations (enable or disable) succeeded and no restart is needed. + /// - OperationSkipped (1): No operations were performed because the current state of all features matched the desired state. + /// - OperationFailed (2): At least one operation failed. + /// + /// Only features present in "validFeatures" are considered valid. If an invalid feature name is encountered, the script exits with OperationFailed. + /// This list should be kept consistent with the list of features in the WindowsOptionalFeatureNames class. + /// + /// + private const string Script = +@" +enum OperationStatus +{ + OperationSucceeded = 0 + OperationSkipped = 1 + OperationFailed = 2 +} + +$validFeatures = @( + 'Containers', + 'HostGuardian', + 'Microsoft-Hyper-V-All', + 'Microsoft-Hyper-V-Tools-All', + 'Microsoft-Hyper-V', + 'VirtualMachinePlatform', + 'HypervisorPlatform', + 'Containers-DisposableClientVM', + 'Microsoft-Windows-Subsystem-Linux' +) + +function ModifyFeatures($featuresString) +{ + $features = ConvertFrom-StringData $featuresString + $anyOperationFailed = $false + $anyOperationPerformed = $false + + foreach ($feature in $features.GetEnumerator()) + { + $featureName = $feature.Key + if ($featureName -notin $validFeatures) + { + exit [OperationStatus]::OperationFailed + } + + $isEnabled = [bool]::Parse($feature.Value); + $featureState = Get-WindowsOptionalFeature -FeatureName $featureName -Online | Select-Object -ExpandProperty State; + $currentEnabled = $featureState -eq 'Enabled'; + + if ($currentEnabled -ne $isEnabled) + { + $anyOperationPerformed = $true + if ($isEnabled) + { + $enableResult = Enable-WindowsOptionalFeature -Online -FeatureName $featureName -All -NoRestart + if ($enableResult -eq $null) + { + $anyOperationFailed = $true + } + } + else + { + $disableResult = Disable-WindowsOptionalFeature -Online -FeatureName $featureName -NoRestart + if ($disableResult -eq $null) + { + $anyOperationFailed = $true + } + } + } + } + + if ($anyOperationFailed) + { + exit [OperationStatus]::OperationFailed; + } + elseif ($anyOperationPerformed) + { + exit [OperationStatus]::OperationSucceeded; + } + { + exit [OperationStatus]::OperationSkipped; + } +} + +ModifyFeatures FEATURE_STRING_INPUT; +"; +} diff --git a/common/Strings/en-us/Resources.resw b/common/Strings/en-us/Resources.resw index a2d1803b91..b563af6c7d 100644 --- a/common/Strings/en-us/Resources.resw +++ b/common/Strings/en-us/Resources.resw @@ -323,4 +323,40 @@ Choose file + + Provides services and tools to create and manage Windows Server Containers and their resources. + Description for the Containers optional feature. + + + Enables the device to create and run Shielded Virtual Machine using remote attestation. + Description for the HostGuardian optional feature. + + + Provides services and management tools for creating and running virtual machines and their resources. + Description for the Microsoft-Hyper-V-All optional feature. + + + Includes GUI and command-line tools for managing Hyper-V. + Description for the Microsoft-Hyper-V-Tools-All optional feature. + + + Provides the services that you can use to create and manage virtual machines and their resources. + Description for the Microsoft-Hyper-V optional feature. + + + Enables platform support for virtual machines + Description for the VirtualMachinePlatform optional feature. + + + Enables virtualization software to run on the Windows hypervisor + Description for the WindowsHypervisorPlatform optional feature. + + + Enables the dependencies required to run Windows Sandbox scenarios. + Description for the Containers-DisposableClientVM optional feature. + + + Provides services and environments for running native user-mode Linux shells and tools on Windows. + Description for the Containers-Microsoft-Windows-Subsystem-Linux optional feature. + \ No newline at end of file diff --git a/common/TelemetryEvents/ModifyWindowsOptionalFeaturesEvent.cs b/common/TelemetryEvents/ModifyWindowsOptionalFeaturesEvent.cs new file mode 100644 index 0000000000..0c02630ee0 --- /dev/null +++ b/common/TelemetryEvents/ModifyWindowsOptionalFeaturesEvent.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; +using static DevHome.Common.Scripts.ModifyWindowsOptionalFeatures; + +namespace DevHome.Common.TelemetryEvents; + +[EventData] +public class ModifyWindowsOptionalFeaturesEvent : EventBase +{ + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServicePerformance; + + public string FeaturesString + { + get; + } + + public string ExitCode + { + get; + } + + public long DurationMs + { + get; + } + + private readonly string[] exitCodeStrings = +[ + "Success", + "NoChange", + "Failure", + "Cancelled", + ]; + + public ModifyWindowsOptionalFeaturesEvent(string featureString, ExitCode result, long durationMs) + { + FeaturesString = featureString; + ExitCode = exitCodeStrings[(int)result]; + DurationMs = durationMs; + } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // No sensitive strings to replace. + } + + public static void Log(string featureString, ExitCode result, long durationMs) + { + TelemetryFactory.Get().Log( + "ModifyVirtualizationFeatures_Event", + LogLevel.Measure, + new ModifyWindowsOptionalFeaturesEvent(featureString, result, durationMs)); + } +} diff --git a/tools/Customization/DevHome.Customization/DevHome.Customization.csproj b/tools/Customization/DevHome.Customization/DevHome.Customization.csproj index 993ba640cd..e3be0129f0 100644 --- a/tools/Customization/DevHome.Customization/DevHome.Customization.csproj +++ b/tools/Customization/DevHome.Customization/DevHome.Customization.csproj @@ -1,61 +1,71 @@ - - - - DevHome.Customization - x86;x64;arm64 - win-x86;win-x64;win-arm64 - enable - true - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - MSBuild:Compile - $(DefaultXamlRuntime) - - - MSBuild:Compile - $(DefaultXamlRuntime) - - - MSBuild:Compile - $(DefaultXamlRuntime) - - - MSBuild:Compile - $(DefaultXamlRuntime) - - - - - $(DefineConstants);DEBUG - - + + + + DevHome.Customization + x86;x64;arm64 + win-x86;win-x64;win-arm64 + enable + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + $(DefaultXamlRuntime) + + + MSBuild:Compile + $(DefaultXamlRuntime) + + + MSBuild:Compile + $(DefaultXamlRuntime) + + + MSBuild:Compile + $(DefaultXamlRuntime) + + + MSBuild:Compile + $(DefaultXamlRuntime) + + + MSBuild:Compile + $(DefaultXamlRuntime) + + + + + $(DefineConstants);DEBUG + + diff --git a/tools/Customization/DevHome.Customization/Extensions/PageExtensions.cs b/tools/Customization/DevHome.Customization/Extensions/PageExtensions.cs index 0973a53f63..09fd989fb9 100644 --- a/tools/Customization/DevHome.Customization/Extensions/PageExtensions.cs +++ b/tools/Customization/DevHome.Customization/Extensions/PageExtensions.cs @@ -1,17 +1,19 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using DevHome.Common.Services; -using DevHome.Customization.ViewModels; -using DevHome.Customization.Views; - -namespace DevHome.Customization.Extensions; - -public static class PageExtensions -{ - public static void ConfigureCustomizationPages(this IPageService pageService) - { - pageService.Configure(); - pageService.Configure(); - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Services; +using DevHome.Customization.ViewModels; +using DevHome.Customization.Views; + +namespace DevHome.Customization.Extensions; + +public static class PageExtensions +{ + public static void ConfigureCustomizationPages(this IPageService pageService) + { + pageService.Configure(); + pageService.Configure(); + pageService.Configure(); + pageService.Configure(); + } +} diff --git a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs index 1372f2b018..8599f8f222 100644 --- a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs +++ b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs @@ -1,33 +1,39 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using DevHome.Customization.ViewModels; -using DevHome.Customization.ViewModels.DevDriveInsights; -using DevHome.Customization.Views; -using DevHome.QuietBackgroundProcesses.UI.ViewModels; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace DevHome.Customization.Extensions; - -public static class ServiceExtensions -{ - public static IServiceCollection AddWindowsCustomization(this IServiceCollection services, HostBuilderContext context) - { - services.AddSingleton(); - services.AddTransient(); - - services.AddSingleton(); - services.AddTransient(); - - services.AddSingleton(sp => - (cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters) => - ActivatorUtilities.CreateInstance(sp, cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters)); - services.AddSingleton(); - services.AddTransient(); - - services.AddTransient(); - - return services; - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Customization.ViewModels; +using DevHome.Customization.ViewModels.DevDriveInsights; +using DevHome.Customization.Views; +using DevHome.QuietBackgroundProcesses.UI.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace DevHome.Customization.Extensions; + +public static class ServiceExtensions +{ + public static IServiceCollection AddWindowsCustomization(this IServiceCollection services, HostBuilderContext context) + { + services.AddSingleton(); + services.AddTransient(); + + services.AddSingleton(); + services.AddTransient(); + + services.AddSingleton(sp => + (cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters) => + ActivatorUtilities.CreateInstance(sp, cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters)); + services.AddSingleton(); + services.AddTransient(); + + services.AddTransient(); + + services.AddSingleton(); + services.AddTransient(); + + services.AddSingleton(); + services.AddTransient(); + + return services; + } +} diff --git a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw index 7485d5d508..89820e9dc9 100644 --- a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw +++ b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw @@ -1,304 +1,392 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Browse - Browse to choose directory - - - Choose a directory - Choose directory - - - Choose directory on Dev Drive... - Choose directory on dev drive - - - Selected directory must be on a Dev Drive - Chosen directory not on dev drive - - - {0} {1} free - Dev drive size free - - - All things Dev Drive, optimizations, etc. - The description for the Dev Drive Insights settings card - - - Dev Drive insights - The header for the Dev Drive Insights settings card - - - Dev Drive insights - Header for Dev Drive insights page in the breadcrumb bar - - - Suggestion - Dev drive optimization suggestion - - - Optimized - Dev drive optimized indicator - - - Size - {0} {1} - Dev drive size - - - {0} {1} used - Dev drive size used - - - Dev Drive volumes - Header for Dev Drive volumes list in Dev Drive insights page - - - Trusted - Dev drive is trusted - - - Untrusted - Dev drive is not trusted - - - Adjust these settings for a more developer-friendly experience using File Explorer. - The description for the File Explorer settings card - - - File Explorer - The header for the File Explorer settings card - - - File Explorer - Header for the File Explorer page in the breadcrumb bar - - - Enable end task in taskbar by right click - The description for the end task on task bar settings card - - - Example: - Example string, will be followed by a sample location to move the cache to a dev drive location - - - End Task - The header for the end task on task bar settings card - - - Windows customization - Header for main Windows customization page in the breadcrumb bar - - - Make changes - Make changes button on dialog - - - Make the change - Make the change button on optimizer card - - - Trust - Make dev drive trusted - - - Additional settings below can be configured in the system settings app - Description and context for a group of Windows settings in the system settings app - - - More Windows settings - Section header for a group of Windows settings in the system settings app - - - Windows customization - Navigation pane content - - - The environment variable {0} is set to {1} - Optimized dev drive description - - - Contents of {0} will be copied to chosen directory. And {1} will be set to chosen directory. - Optimize dev drive dialog description - - - Global environment variable {0} is set to {1}, which is not located on Dev Drive. Move contents of this folder to a directory on Dev Drive such as {2} and set {3} to that chosen directory on Dev Drive - Optimizer dev drive description - - - Move contents of {0} to a directory on Dev Drive such as {1} and set {2} to that chosen directory on Dev Drive - Optimizer dev drive description when environment variable is not set - - - A package cache is the global folder location used by an application to store files for installed software. These source files are needed when you want to update, uninstall, or repair the installed software. - Package caches description - - - Learn more - Package caches hyperlink text - - - Search - The placholder text for the settings auto suggest box - - - Show empty drives - The header for the show empty drives settings card - - - Show file extensions - The header for the show file extensions setting. - - - Show files after extraction is complete - The header for show files after extraction is complete setting - - - Show full path in title bar - The header for the show full path in title bar settings card - - - Show hidden and system files - The header for the show hidden and system files setting. - - - Microsoft Defender performance mode is only available for trusted Dev Drive volumes - The meaning of untrusted dev drives. - - - Untrusted Dev Drive - Untrusted Dev Drive - - - Learn more - Learn more link for untrusted dev drives. - - - Open the Windows Settings app - The action tool tip for the Windows developer settings card - - - Windows Settings intended for development use only. - The description for the Windows developer settings card - - - Windows developer settings - The header for the Windows developer settings card - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Browse + Browse to choose directory + + + Choose a directory + Choose directory + + + Choose directory on Dev Drive... + Choose directory on dev drive + + + Selected directory must be on a Dev Drive + Chosen directory not on dev drive + + + {0} {1} free + Dev drive size free + + + All things Dev Drive, optimizations, etc. + The description for the Dev Drive Insights settings card + + + Dev Drive insights + The header for the Dev Drive Insights settings card + + + Dev Drive insights + Header for Dev Drive insights page in the breadcrumb bar + + + Suggestion + Dev drive optimization suggestion + + + Optimized + Dev drive optimized indicator + + + Size - {0} {1} + Dev drive size + + + {0} {1} used + Dev drive size used + + + Dev Drive volumes + Header for Dev Drive volumes list in Dev Drive insights page + + + Trusted + Dev drive is trusted + + + Untrusted + Dev drive is not trusted + + + Adjust these settings for a more developer-friendly experience using File Explorer. + The description for the File Explorer settings card + + + File Explorer + The header for the File Explorer settings card + + + File Explorer + Header for the File Explorer page in the breadcrumb bar + + + Enable end task in taskbar by right click + The description for the end task on task bar settings card + + + Example: + Example string, will be followed by a sample location to move the cache to a dev drive location + + + End Task + The header for the end task on task bar settings card + + + Windows customization + Header for main Windows customization page in the breadcrumb bar + + + Make changes + Make changes button on dialog + + + Make the change + Make the change button on optimizer card + + + Trust + Make dev drive trusted + + + Additional settings below can be configured in the system settings app + Description and context for a group of Windows settings in the system settings app + + + More Windows settings + Section header for a group of Windows settings in the system settings app + + + Windows customization + Navigation pane content + + + The environment variable {0} is set to {1} + Optimized dev drive description + + + Contents of {0} will be copied to chosen directory. And {1} will be set to chosen directory. + Optimize dev drive dialog description + + + Global environment variable {0} is set to {1}, which is not located on Dev Drive. Move contents of this folder to a directory on Dev Drive such as {2} and set {3} to that chosen directory on Dev Drive + Optimizer dev drive description + + + Move contents of {0} to a directory on Dev Drive such as {1} and set {2} to that chosen directory on Dev Drive + Optimizer dev drive description when environment variable is not set + + + A package cache is the global folder location used by an application to store files for installed software. These source files are needed when you want to update, uninstall, or repair the installed software. + Package caches description + + + Learn more + Package caches hyperlink text + + + Search + The placholder text for the settings auto suggest box + + + Show empty drives + The header for the show empty drives settings card + + + Show file extensions + The header for the show file extensions setting. + + + Show files after extraction is complete + The header for show files after extraction is complete setting + + + Show full path in title bar + The header for the show full path in title bar settings card + + + Show hidden and system files + The header for the show hidden and system files setting. + + + Microsoft Defender performance mode is only available for trusted Dev Drive volumes + The meaning of untrusted dev drives. + + + Untrusted Dev Drive + Untrusted Dev Drive + + + Learn more + Learn more link for untrusted dev drives. + + + Open the Windows Settings app + The action tool tip for the Windows developer settings card + + + Windows Settings intended for development use only. + The description for the Windows developer settings card + + + Windows developer settings + The header for the Windows developer settings card + + + Virtualization feature management + Header for main Windows customization page in the breadcrumb bar + + + Enable or disable optional Windows virtualization features + The description for the virtualization feature management settings card + + + Virtualization feature management + The header for the virtualization feature management settings card + + + Apply + Apply changes to feature state. + + + Committing changes + Title displayed during the process of applying changes + + + Please wait for the changes to take effect. + Message indicating the user should wait for the changes to be applied + + + Restart now + Button text to restart the system immediately + + + Cancel + Button text to cancel the current operation + + + Restart required + Title displayed when a restart is required to apply changes + + + Please restart your machine for your applied changes to take effect. + Message instructing the user to restart their machine to apply changes + + + Don't restart now + Button text to postpone system restart + + + Changes applied + The title for a notification shown to the user to indicate that changes have been successfully applied. + + + Restart the computer and try again. + The message for a notification shown to the user to indicate that some changes failed to apply and that restarting the computer is the most likely solution. + + + Failed to apply some changes + The title for a notification shown to the user to indicate that some, but maybe not all, changes failed to apply. Usually this is due to the system being in an invalid state, such as a Windows Update being queued. + + + Only users with the Administrator role can modify optional features. + The message for a notification shown to the user to further describe that only users with the Administrator role in Windows can modify optional feature state. + + + The current user is not an administrator + The title for a notification shown to the user to indicate that the current Windows user is not an administrator, meaning they are not able to change certain settings. + + + Configure various system settings such as long paths and taskbar customization. + The description for the General System settings card + + + General system + The header for the General System settings card + + + General system + Header for the General system page in the breadcrumb bar + + + Remove MAX_PATH limitations from common Win32 file and directory functions. + The description for the long paths settings card + + + Enable long paths + The header for the long paths settings card + + + A restart may be required for long path changes to take effect. + Message instructing the user to restart their machine to apply changes + \ No newline at end of file diff --git a/tools/Customization/DevHome.Customization/ViewModels/FileExplorerViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/FileExplorerViewModel.cs index 5879cedc2a..cbc7319c3d 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/FileExplorerViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/FileExplorerViewModel.cs @@ -1,91 +1,81 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.ObjectModel; -using CommunityToolkit.Mvvm.ComponentModel; -using DevHome.Common.Models; -using DevHome.Common.Services; -using DevHome.Customization.Models; -using DevHome.Customization.TelemetryEvents; -using Microsoft.Internal.Windows.DevHome.Helpers; - -namespace DevHome.Customization.ViewModels; - -public partial class FileExplorerViewModel : ObservableObject -{ - private readonly ShellSettings _shellSettings; - - public ObservableCollection Breadcrumbs { get; } - - public FileExplorerViewModel() - { - _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("FileExplorer_Header"), typeof(FileExplorerViewModel).FullName!) - ]; - } - - public bool ShowFileExtensions - { - get => FileExplorerSettings.ShowFileExtensionsEnabled(); - set - { - SettingChangedEvent.Log("ShowFileExtensions", value.ToString()); - FileExplorerSettings.SetShowFileExtensionsEnabled(value); - } - } - - public bool ShowHiddenAndSystemFiles - { - get => FileExplorerSettings.ShowHiddenAndSystemFilesEnabled(); - set - { - SettingChangedEvent.Log("ShowHiddenAndSystemFiles", value.ToString()); - FileExplorerSettings.SetShowHiddenAndSystemFilesEnabled(value); - } - } - - public bool ShowFullPathInTitleBar - { - get => FileExplorerSettings.ShowFullPathInTitleBarEnabled(); - set - { - SettingChangedEvent.Log("ShowFullPathInTitleBar", value.ToString()); - FileExplorerSettings.SetShowFullPathInTitleBarEnabled(value); - } - } - - public bool ShowEmptyDrives - { - get => _shellSettings.ShowEmptyDrivesEnabled(); - set - { - SettingChangedEvent.Log("ShowEmptyDrives", value.ToString()); - _shellSettings.SetShowEmptyDrivesEnabled(value); - } - } - - public bool ShowFilesAfterExtraction - { - get => _shellSettings.ShowFilesAfterExtractionEnabled(); - set - { - SettingChangedEvent.Log("ShowFilesAfterExtraction", value.ToString()); - _shellSettings.SetShowFilesAfterExtractionEnabled(value); - } - } - - public bool EndTaskOnTaskBarEnabled - { - get => _shellSettings.EndTaskOnTaskBarEnabled(); - set - { - SettingChangedEvent.Log("EndTaskOnTaskBarEnabled", value.ToString()); - _shellSettings.SetEndTaskOnTaskBarEnabled(value); - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Models; +using DevHome.Common.Services; +using DevHome.Customization.Models; +using DevHome.Customization.TelemetryEvents; +using Microsoft.Internal.Windows.DevHome.Helpers; + +namespace DevHome.Customization.ViewModels; + +public partial class FileExplorerViewModel : ObservableObject +{ + private readonly ShellSettings _shellSettings; + + public ObservableCollection Breadcrumbs { get; } + + public FileExplorerViewModel() + { + _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("FileExplorer_Header"), typeof(FileExplorerViewModel).FullName!) + ]; + } + + public bool ShowFileExtensions + { + get => FileExplorerSettings.ShowFileExtensionsEnabled(); + set + { + SettingChangedEvent.Log("ShowFileExtensions", value.ToString()); + FileExplorerSettings.SetShowFileExtensionsEnabled(value); + } + } + + public bool ShowHiddenAndSystemFiles + { + get => FileExplorerSettings.ShowHiddenAndSystemFilesEnabled(); + set + { + SettingChangedEvent.Log("ShowHiddenAndSystemFiles", value.ToString()); + FileExplorerSettings.SetShowHiddenAndSystemFilesEnabled(value); + } + } + + public bool ShowFullPathInTitleBar + { + get => FileExplorerSettings.ShowFullPathInTitleBarEnabled(); + set + { + SettingChangedEvent.Log("ShowFullPathInTitleBar", value.ToString()); + FileExplorerSettings.SetShowFullPathInTitleBarEnabled(value); + } + } + + public bool ShowEmptyDrives + { + get => _shellSettings.ShowEmptyDrivesEnabled(); + set + { + SettingChangedEvent.Log("ShowEmptyDrives", value.ToString()); + _shellSettings.SetShowEmptyDrivesEnabled(value); + } + } + + public bool ShowFilesAfterExtraction + { + get => _shellSettings.ShowFilesAfterExtractionEnabled(); + set + { + SettingChangedEvent.Log("ShowFilesAfterExtraction", value.ToString()); + _shellSettings.SetShowFilesAfterExtractionEnabled(value); + } + } +} diff --git a/tools/Customization/DevHome.Customization/ViewModels/GeneralSystemViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/GeneralSystemViewModel.cs new file mode 100644 index 0000000000..14cdb4d107 --- /dev/null +++ b/tools/Customization/DevHome.Customization/ViewModels/GeneralSystemViewModel.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.WinUI; +using CommunityToolkit.WinUI.Behaviors; +using DevHome.Common.Extensions; +using DevHome.Common.Helpers; +using DevHome.Common.Models; +using DevHome.Common.Scripts; +using DevHome.Common.Services; +using DevHome.Customization.TelemetryEvents; +using Microsoft.Internal.Windows.DevHome.Helpers; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Win32; +using Serilog; + +namespace DevHome.Customization.ViewModels; + +public partial class GeneralSystemViewModel : ObservableObject +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(GeneralSystemViewModel)); + + private readonly DispatcherQueue _dispatcherQueue; + + private readonly StringResource _stringResource; + + private readonly ShellSettings _shellSettings; + + private readonly bool _isUserAdministrator = WindowsIdentityHelper.IsUserAdministrator(); + + public ObservableCollection Breadcrumbs { get; } + + private StackedNotificationsBehavior? _notificationQueue; + + private AsyncRelayCommand ModifyLongPathsCommand { get; } + + public bool CanModifyLongPaths => _isUserAdministrator && !ModifyLongPathsCommand.IsRunning; + + public GeneralSystemViewModel(DispatcherQueue dispatcherQueue) + { + _dispatcherQueue = dispatcherQueue; + _shellSettings = new ShellSettings(); + + _stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); + Breadcrumbs = + [ + new(_stringResource.GetLocalized("MainPage_Header"), typeof(MainPageViewModel).FullName!), + new(_stringResource.GetLocalized("GeneralSystem_Header"), typeof(GeneralSystemViewModel).FullName!) + ]; + + ModifyLongPathsCommand = new AsyncRelayCommand(ModifyLongPathsAsync); + ModifyLongPathsCommand.PropertyChanged += async (s, e) => + { + if (e.PropertyName == nameof(ModifyLongPathsCommand.IsRunning)) + { + await _dispatcherQueue.EnqueueAsync(() => OnPropertyChanged(nameof(CanModifyLongPaths))); + } + }; + } + + public void Initialize(StackedNotificationsBehavior notificationQueue) + { + _notificationQueue = notificationQueue; + } + + public void Uninitialize() + { + _notificationQueue = null; + } + + public bool EndTaskOnTaskBarEnabled + { + get => _shellSettings.EndTaskOnTaskBarEnabled(); + set + { + SettingChangedEvent.Log("EndTaskOnTaskBarEnabled", value.ToString()); + _shellSettings.SetEndTaskOnTaskBarEnabled(value); + } + } + + public bool LongPathsEnabled + { + get => CheckLongPathsEnabled(); + set + { + if (ModifyLongPathsCommand.IsRunning) + { + return; + } + + ModifyLongPathsCommand.ExecuteAsync(value); + } + } + + private bool CheckLongPathsEnabled() + { + const string keyPath = @"SYSTEM\CurrentControlSet\Control\FileSystem"; + const string valueName = "LongPathsEnabled"; + + using var key = Registry.LocalMachine.OpenSubKey(keyPath); + if (key != null) + { + var value = key.GetValue(valueName); + if (value is int intValue) + { + return intValue == 1; + } + } + + return false; + } + + private async Task ModifyLongPathsAsync(bool enabled) + { + await Task.Run(async () => + { + var currentState = CheckLongPathsEnabled(); + if (enabled == currentState) + { + return; + } + + var exitCode = ModifyLongPathsSetting.ModifyLongPaths(enabled, _log); + if (exitCode == ModifyLongPathsSetting.ExitCode.Success) + { + _log?.Information($"Long paths setting {(enabled ? "enabled" : "disabled")} successfully."); + ShowRestartNotification(); + } + else + { + _log?.Error($"Failed to {(enabled ? "enable" : "disable")} long paths setting."); + } + + await _dispatcherQueue.EnqueueAsync(() => OnPropertyChanged(nameof(LongPathsEnabled))); + }); + } + + public void ShowRestartNotification() + { + _notificationQueue?.ShowWithWindowExtension( + _stringResource.GetLocalized("ChangesAppliedTitle"), + _stringResource.GetLocalized("LongPathsChangedRestartMessage"), + InfoBarSeverity.Warning, + new RelayCommand(RestartHelper.RestartComputer), + _stringResource.GetLocalized("RestartNowButtonText")); + } +} diff --git a/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs index de4456a2b1..5a41bc4df9 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs @@ -70,4 +70,16 @@ private void NavigateToDevDriveInsightsPage() { NavigationService.NavigateTo(typeof(DevDriveInsightsViewModel).FullName!); } + + [RelayCommand] + private void NavigateToVirtualizationFeatureManagementPage() + { + NavigationService.NavigateTo(typeof(VirtualizationFeatureManagementViewModel).FullName!); + } + + [RelayCommand] + private void NavigateToGeneralSystemPage() + { + NavigationService.NavigateTo(typeof(GeneralSystemViewModel).FullName!); + } } diff --git a/tools/Customization/DevHome.Customization/ViewModels/ModifyFeaturesDialogViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/ModifyFeaturesDialogViewModel.cs new file mode 100644 index 0000000000..b600477582 --- /dev/null +++ b/tools/Customization/DevHome.Customization/ViewModels/ModifyFeaturesDialogViewModel.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Helpers; +using DevHome.Common.Services; + +namespace DevHome.Customization.ViewModels; + +public partial class ModifyFeaturesDialogViewModel : ObservableObject +{ + private readonly StringResource _stringResource; + + private readonly IAsyncRelayCommand _applyChangesCommand; + + private CancellationTokenSource? _cancellationTokenSource; + + public enum State + { + Initial, + CommittingChanges, + Complete, + } + + [ObservableProperty] + private State _currentState = State.Initial; + + public ModifyFeaturesDialogViewModel(IAsyncRelayCommand applyChangedCommand) + { + _stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); + _applyChangesCommand = applyChangedCommand; + } + + [ObservableProperty] + private string _title = string.Empty; + + [ObservableProperty] + private string _message = string.Empty; + + [ObservableProperty] + private string _primaryButtonText = string.Empty; + + [ObservableProperty] + private string _secondaryButtonText = string.Empty; + + [ObservableProperty] + private bool _isPrimaryButtonEnabled; + + [ObservableProperty] + private bool _isSecondaryButtonEnabled; + + [ObservableProperty] + private bool _showProgress; + + public void SetCommittingChanges(CancellationTokenSource cancellationTokenSource) + { + CurrentState = State.CommittingChanges; + + _cancellationTokenSource = cancellationTokenSource; + IsPrimaryButtonEnabled = false; + IsSecondaryButtonEnabled = true; + ShowProgress = true; + Title = _stringResource.GetLocalized("CommittingChangesTitle"); + Message = _stringResource.GetLocalized("CommittingChangesMessage"); + PrimaryButtonText = _stringResource.GetLocalized("RestartNowButtonText"); + SecondaryButtonText = _stringResource.GetLocalized("CancelButtonText"); + } + + public void SetCompleteRestartRequired() + { + CurrentState = State.Complete; + + _cancellationTokenSource = null; + IsPrimaryButtonEnabled = true; + IsSecondaryButtonEnabled = true; + ShowProgress = false; + Title = _stringResource.GetLocalized("RestartRequiredTitle"); + Message = _stringResource.GetLocalized("RestartRequiredMessage"); + PrimaryButtonText = _stringResource.GetLocalized("RestartNowButtonText"); + SecondaryButtonText = _stringResource.GetLocalized("DontRestartNowButtonText"); + } + + internal void HandlePrimaryButton() + { + switch (CurrentState) + { + case State.Complete: + RestartHelper.RestartComputer(); + break; + } + } + + internal void HandleSecondaryButton() + { + switch (CurrentState) + { + case State.CommittingChanges: + _cancellationTokenSource?.Cancel(); + break; + } + } +} diff --git a/tools/Customization/DevHome.Customization/ViewModels/VirtualizationFeatureManagementViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/VirtualizationFeatureManagementViewModel.cs new file mode 100644 index 0000000000..ff7725a906 --- /dev/null +++ b/tools/Customization/DevHome.Customization/ViewModels/VirtualizationFeatureManagementViewModel.cs @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.WinUI; +using CommunityToolkit.WinUI.Behaviors; +using DevHome.Common.Extensions; +using DevHome.Common.Helpers; +using DevHome.Common.Models; +using DevHome.Common.Scripts; +using DevHome.Common.Services; +using DevHome.Customization.Views; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Serilog; + +namespace DevHome.Customization.ViewModels; + +public partial class VirtualizationFeatureManagementViewModel : ObservableObject +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(VirtualizationFeatureManagementViewModel)); + + private readonly StringResource _stringResource; + + private readonly bool _isUserAdministrator = WindowsIdentityHelper.IsUserAdministrator(); + + private readonly Dictionary _initialFeatureEnabledStates = new(); + + private readonly Window _window; + + private readonly ModifyFeaturesDialog _modifyFeaturesDialog; + + private StackedNotificationsBehavior? _notificationQueue; + + public IAsyncRelayCommand LoadFeaturesCommand { get; } + + public bool FeaturesLoaded => !_isFirstLoad || !LoadFeaturesCommand.IsRunning; + + public IAsyncRelayCommand ApplyChangesCommand { get; } + + public bool ChangesCanBeApplied => HasFeatureChanges && !ApplyChangesCommand.IsRunning; + + public ObservableCollection Breadcrumbs { get; } + + public ObservableCollection Features { get; } = new(); + + public bool HasFeatureChanges => _isUserAdministrator && FeaturesLoaded && Features.Any(f => f.HasChanged); + + public bool CanDismissNotifications => _isUserAdministrator; + + private bool _restartNeeded; + + private bool _isFirstLoad; + + public VirtualizationFeatureManagementViewModel(Window window) + { + _stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); + _window = window; + _isFirstLoad = true; + + Breadcrumbs = + [ + new(_stringResource.GetLocalized("MainPage_Header"), typeof(MainPageViewModel).FullName!), + new(_stringResource.GetLocalized("VirtualizationFeatureManagement_Header"), typeof(VirtualizationFeatureManagementViewModel).FullName!) + ]; + + LoadFeaturesCommand = new AsyncRelayCommand(LoadFeaturesAsync); + LoadFeaturesCommand.PropertyChanged += async (s, e) => + { + if (e.PropertyName == nameof(LoadFeaturesCommand.IsRunning)) + { + await OnFeaturesChanged(); + } + }; + + ApplyChangesCommand = new AsyncRelayCommand(ApplyChangesAsync); + ApplyChangesCommand.PropertyChanged += async (s, e) => + { + if (e.PropertyName == nameof(ApplyChangesCommand.IsRunning)) + { + await OnFeaturesChanged(); + } + }; + + _modifyFeaturesDialog = new ModifyFeaturesDialog(ApplyChangesCommand) + { + XamlRoot = _window.Content.XamlRoot, + }; + } + + internal async Task Initialize(StackedNotificationsBehavior notificationQueue) + { + _notificationQueue = notificationQueue; + + var loadingTask = LoadFeaturesCommand.ExecuteAsync(null); + + if (!_isUserAdministrator) + { + await _window.DispatcherQueue.EnqueueAsync(ShowNonAdminUserNotification); + } + + if (_restartNeeded) + { + await _window.DispatcherQueue.EnqueueAsync(ShowRestartNotification); + } + + await loadingTask; + } + + internal void Uninitialize() + { + _notificationQueue = null; + } + + private async void FeatureState_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(WindowsOptionalFeatureState.IsEnabled)) + { + await OnFeaturesChanged(); + } + } + + private async Task LoadFeaturesAsync() + { + var tempFeatures = new ObservableCollection(); + + await Task.Run(() => + { + foreach (var featureName in WindowsOptionalFeatures.VirtualMachineFeatures) + { + var feature = ManagementInfrastructureHelper.GetWindowsFeatureDetails(featureName); + if (feature != null && feature.IsAvailable) + { + // A features is consider modifiable if the user is an administrator. + var featureState = new WindowsOptionalFeatureState(feature, _isUserAdministrator); + featureState.PropertyChanged += FeatureState_PropertyChanged; + + // Add to the temporary list instead of the main Features collection to avoid the UI updating + // for each feature added since GetWindowsFeatureDetails can take some time. + tempFeatures.Add(featureState); + + // Keep track of the original feature state to determine if changes have been made to provide + // a notification to the user if they try to navigate away without applying changes or restarting + // when needed. + if (!_initialFeatureEnabledStates.ContainsKey(featureName)) + { + _initialFeatureEnabledStates.Add(featureName, featureState.IsEnabled); + } + } + } + + return Task.CompletedTask; + }); + + // Update the Features collection all at once + await _window.DispatcherQueue.EnqueueAsync(() => + { + Features.Clear(); + foreach (var featureState in tempFeatures) + { + Features.Add(featureState); + } + }); + + // After the first load, set _isFirstLoad to false so that the list is not hidden after the initial + // load from an empty page. Subsequent loads will keep the list visible if any changes are made. + if (_isFirstLoad) + { + _isFirstLoad = false; + } + + await OnFeaturesChanged(); + } + + private bool HasFeatureStatusChanged() + { + foreach (var feature in Features) + { + if (_initialFeatureEnabledStates.TryGetValue(feature.Feature.FeatureName, out var initialState)) + { + if (initialState != feature.IsEnabled) + { + return true; + } + } + } + + return false; + } + + private async Task ApplyChangesAsync() + { + _notificationQueue?.ClearWithWindowExtension(); + + var cancellationTokenSource = new CancellationTokenSource(); + _modifyFeaturesDialog.ViewModel.SetCommittingChanges(cancellationTokenSource); + + var showDialogTask = _modifyFeaturesDialog.ShowAsync(); + + await _window.DispatcherQueue.EnqueueAsync(async () => + { + var exitCode = await ModifyWindowsOptionalFeatures.ModifyFeaturesAsync(Features, _log, cancellationTokenSource.Token); + + await LoadFeaturesCommand.ExecuteAsync(null); + _restartNeeded = HasFeatureStatusChanged(); + if (_restartNeeded) + { + ShowRestartNotification(); + } + + switch (exitCode) + { + case ModifyWindowsOptionalFeatures.ExitCode.Success: + // Mark that changes have been applied and a restart is needed. This allows for a persistent notification + // to be displayed when the user navigates away from the page and returns. + if (_restartNeeded) + { + _modifyFeaturesDialog.ViewModel.SetCompleteRestartRequired(); + } + else + { + _modifyFeaturesDialog.Hide(); + } + + break; + case ModifyWindowsOptionalFeatures.ExitCode.Failure: + _modifyFeaturesDialog.Hide(); + ShowFailedToApplyAllNotification(); + break; + case ModifyWindowsOptionalFeatures.ExitCode.Cancelled: + case ModifyWindowsOptionalFeatures.ExitCode.NoChange: + // Do nothing for these conditions, the InfoBar will be updated by ModifyFeaturesAsync + // in these cases. + _modifyFeaturesDialog.Hide(); + break; + } + }); + + await showDialogTask; + } + + private async Task OnFeaturesChanged() + { + await _window.DispatcherQueue.EnqueueAsync(() => + { + OnPropertyChanged(nameof(FeaturesLoaded)); + OnPropertyChanged(nameof(HasFeatureChanges)); + OnPropertyChanged(nameof(ChangesCanBeApplied)); + }); + } + + public void ShowRestartNotification() + { + _notificationQueue?.ShowWithWindowExtension( + _stringResource.GetLocalized("ChangesAppliedTitle"), + _stringResource.GetLocalized("RestartRequiredMessage"), + InfoBarSeverity.Warning, + new RelayCommand(RestartHelper.RestartComputer), + _stringResource.GetLocalized("RestartNowButtonText")); + } + + public void ShowNonAdminUserNotification() + { + _notificationQueue?.ShowWithWindowExtension( + _stringResource.GetLocalized("NonAdminUserTitle"), + _stringResource.GetLocalized("NonAdminUserMessage"), + InfoBarSeverity.Warning); + } + + public void ShowFailedToApplyAllNotification() + { + _notificationQueue?.ShowWithWindowExtension( + _stringResource.GetLocalized("FailedToApplyChangesTitle"), + _stringResource.GetLocalized("FailedToApplyChangesMessage"), + InfoBarSeverity.Error); + } +} diff --git a/tools/Customization/DevHome.Customization/Views/FileExplorerView.xaml b/tools/Customization/DevHome.Customization/Views/FileExplorerView.xaml index 2b1037490a..ab2fda9dfc 100644 --- a/tools/Customization/DevHome.Customization/Views/FileExplorerView.xaml +++ b/tools/Customization/DevHome.Customization/Views/FileExplorerView.xaml @@ -1,29 +1,23 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml b/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml new file mode 100644 index 0000000000..b8892310c2 --- /dev/null +++ b/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml.cs b/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml.cs new file mode 100644 index 0000000000..5617b267fd --- /dev/null +++ b/tools/Customization/DevHome.Customization/Views/GeneralSystemPage.xaml.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Extensions; +using DevHome.Common.Views; +using DevHome.Customization.ViewModels; +using Microsoft.UI.Xaml; + +namespace DevHome.Customization.Views; + +public sealed partial class GeneralSystemPage : DevHomePage +{ + public GeneralSystemViewModel ViewModel + { + get; + } + + public GeneralSystemPage() + { + ViewModel = Application.Current.GetService(); + this.InitializeComponent(); + } +} diff --git a/tools/Customization/DevHome.Customization/Views/GeneralSystemView.xaml b/tools/Customization/DevHome.Customization/Views/GeneralSystemView.xaml new file mode 100644 index 0000000000..4d30669666 --- /dev/null +++ b/tools/Customization/DevHome.Customization/Views/GeneralSystemView.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + diff --git a/tools/Customization/DevHome.Customization/Views/GeneralSystemView.xaml.cs b/tools/Customization/DevHome.Customization/Views/GeneralSystemView.xaml.cs new file mode 100644 index 0000000000..855a5756c3 --- /dev/null +++ b/tools/Customization/DevHome.Customization/Views/GeneralSystemView.xaml.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Extensions; +using DevHome.Customization.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Customization.Views; + +public sealed partial class GeneralSystemView : UserControl +{ + public GeneralSystemViewModel ViewModel + { + get; + } + + public GeneralSystemView() + { + InitializeComponent(); + + ViewModel = Application.Current.GetService(); + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + ViewModel.Initialize(NotificationQueue); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + ViewModel.Uninitialize(); + } +} diff --git a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml index 9e957a96f2..edef59ee61 100644 --- a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml +++ b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml @@ -1,61 +1,81 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/Customization/DevHome.Customization/Views/ModifyFeaturesDialog.xaml b/tools/Customization/DevHome.Customization/Views/ModifyFeaturesDialog.xaml new file mode 100644 index 0000000000..4218a7fe29 --- /dev/null +++ b/tools/Customization/DevHome.Customization/Views/ModifyFeaturesDialog.xaml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/tools/Customization/DevHome.Customization/Views/ModifyFeaturesDialog.xaml.cs b/tools/Customization/DevHome.Customization/Views/ModifyFeaturesDialog.xaml.cs new file mode 100644 index 0000000000..ad10d1ca27 --- /dev/null +++ b/tools/Customization/DevHome.Customization/Views/ModifyFeaturesDialog.xaml.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.Input; +using DevHome.Customization.ViewModels; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.Customization.Views; + +public sealed partial class ModifyFeaturesDialog : ContentDialog +{ + public ModifyFeaturesDialogViewModel ViewModel { get; } + + public ModifyFeaturesDialog(IAsyncRelayCommand applyChangedCommand) + { + ViewModel = new ModifyFeaturesDialogViewModel(applyChangedCommand); + this.InitializeComponent(); + this.DataContext = ViewModel; + } + + private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + ViewModel.HandlePrimaryButton(); + } + + private void OnSecondaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + ViewModel.HandleSecondaryButton(); + } +} diff --git a/tools/Customization/DevHome.Customization/Views/VirtualizationFeatureManagementPage.xaml b/tools/Customization/DevHome.Customization/Views/VirtualizationFeatureManagementPage.xaml new file mode 100644 index 0000000000..f86e91b36b --- /dev/null +++ b/tools/Customization/DevHome.Customization/Views/VirtualizationFeatureManagementPage.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +