| 1 | // Copyright (C) Microsoft Corporation. All rights reserved. |
| 2 | |
| 3 | using System; |
| 4 | using System.ComponentModel; |
| 5 | using System.Diagnostics; |
| 6 | using System.Runtime.InteropServices; |
| 7 | using Microsoft.UI.Xaml; |
| 8 | using Microsoft.UI.Xaml.Controls; |
| 9 | using WslSettings.Contracts.Services; |
| 10 | |
| 11 | namespace WslSettings.Views.Settings; |
| 12 | |
| 13 | internal static class SettingsApplyHelper |
| 14 | { |
| 15 | public static async Task ShowApplyChangesDialogAsync(XamlRoot xamlRoot) |
| 16 | { |
| 17 | var wslConfigService = App.GetService<IWslConfigService>(); |
| 18 | var pendingChanges = wslConfigService.GetPendingChanges(); |
| 19 | if (pendingChanges.Count == 0) |
| 20 | { |
| 21 | return; |
| 22 | } |
| 23 | |
| 24 | var changeLines = new List<string>(pendingChanges.Count); |
| 25 | foreach (var change in pendingChanges) |
| 26 | { |
| 27 | changeLines.Add($"- {GetSettingDisplayName(change.ConfigEntry)}: {FormatValue(change.ConfigEntry, change.PendingValue)}"); |
| 28 | } |
| 29 | |
| 30 | var contentText = string.Join(Environment.NewLine, changeLines); |
| 31 | |
| 32 | var contentPanel = new StackPanel { Spacing = 8 }; |
| 33 | contentPanel.Children.Add(new TextBlock |
| 34 | { |
| 35 | Text = "Settings_ApplyChangesDialogDescription".GetLocalized(), |
| 36 | TextWrapping = TextWrapping.Wrap, |
| 37 | }); |
| 38 | contentPanel.Children.Add(new TextBlock |
| 39 | { |
| 40 | Text = contentText, |
| 41 | TextWrapping = TextWrapping.Wrap, |
| 42 | FontWeight = Microsoft.UI.Text.FontWeights.SemiBold, |
| 43 | }); |
| 44 | |
| 45 | var dialog = new ContentDialog |
| 46 | { |
| 47 | XamlRoot = xamlRoot, |
| 48 | Title = "Settings_ApplyChangesDialogTitle".GetLocalized(), |
| 49 | Content = contentPanel, |
| 50 | PrimaryButtonText = "Settings_ApplyChangesDialogShutdownButton".GetLocalized(), |
| 51 | SecondaryButtonText = "Settings_ApplyChangesDialogLaterButton".GetLocalized(), |
| 52 | CloseButtonText = "Settings_ApplyChangesDialogCloseButton".GetLocalized(), |
| 53 | DefaultButton = ContentDialogButton.Primary, |
| 54 | }; |
| 55 | |
| 56 | var result = await dialog.ShowAsync(); |
| 57 | if (result == ContentDialogResult.Primary) |
| 58 | { |
| 59 | // "Shutdown WSL now" — commit to disk and shutdown |
| 60 | var commitResult = wslConfigService.CommitPendingChanges(); |
| 61 | if (commitResult != 0) |
| 62 | { |
| 63 | await ShowFailureDialogAsync(xamlRoot, string.Format("Settings_ApplyChangesDialogCommitFailed".GetLocalized(), commitResult)); |
| 64 | return; |
| 65 | } |
| 66 | |
| 67 | try |
| 68 | { |
| 69 | var wslPath = Path.Combine(AppContext.BaseDirectory, "..", "wsl.exe"); |
| 70 | Process.Start(new ProcessStartInfo |
| 71 | { |
| 72 | FileName = wslPath, |
| 73 | Arguments = "--shutdown", |
| 74 | CreateNoWindow = true, |
| 75 | UseShellExecute = false, |
| 76 | })?.Dispose(); |
| 77 | } |
| 78 | catch (Win32Exception ex) |
| 79 | { |
| 80 | await ShowFailureDialogAsync(xamlRoot, string.Format("Settings_ApplyChangesDialogShutdownFailed".GetLocalized(), ex.Message)); |
| 81 | } |
| 82 | catch (InvalidOperationException ex) |
| 83 | { |
| 84 | await ShowFailureDialogAsync(xamlRoot, string.Format("Settings_ApplyChangesDialogShutdownFailed".GetLocalized(), ex.Message)); |
| 85 | } |
| 86 | } |
| 87 | else if (result == ContentDialogResult.Secondary) |
| 88 | { |
| 89 | // "Later" — commit to disk but don't shutdown; settings apply on next WSL restart |
| 90 | var commitResult = wslConfigService.CommitPendingChanges(); |
| 91 | if (commitResult != 0) |
| 92 | { |
| 93 | await ShowFailureDialogAsync(xamlRoot, string.Format("Settings_ApplyChangesDialogCommitFailed".GetLocalized(), commitResult)); |
| 94 | } |
| 95 | } |
| 96 | // Esc / close — do nothing, leave changes pending |
| 97 | } |
| 98 | |
| 99 | private static async Task ShowFailureDialogAsync(XamlRoot xamlRoot, string message) |
| 100 | { |
| 101 | var dialog = new ContentDialog |
| 102 | { |
| 103 | XamlRoot = xamlRoot, |
| 104 | Title = "Settings_ApplyChangesDialogFailedTitle".GetLocalized(), |
| 105 | Content = new TextBlock |
| 106 | { |
| 107 | Text = message, |
| 108 | TextWrapping = TextWrapping.Wrap, |
| 109 | MaxWidth = 620, |
| 110 | }, |
| 111 | CloseButtonText = "Settings_ApplyChangesDialogCloseButton".GetLocalized(), |
| 112 | DefaultButton = ContentDialogButton.Close, |
| 113 | }; |
| 114 | |
| 115 | await dialog.ShowAsync(); |
| 116 | } |
| 117 | |
| 118 | private static string GetSettingDisplayName(WslConfigEntry entry) |
| 119 | { |
| 120 | // Use existing Settings page resource keys so dialog matches page terminology |
| 121 | if (SettingDisplayNameResources.TryGetValue(entry, out var resourceKey)) |
| 122 | { |
| 123 | var localized = resourceKey.GetLocalized(); |
| 124 | if (!string.IsNullOrEmpty(localized) && localized != resourceKey) |
| 125 | { |
| 126 | return localized; |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | // Fallback: type name (keeps something useful even if resx missing) |
| 131 | return entry.ToString(); |
| 132 | } |
| 133 | |
| 134 | private static readonly IReadOnlyDictionary<WslConfigEntry, string> SettingDisplayNameResources = |
| 135 | new Dictionary<WslConfigEntry, string> |
| 136 | { |
| 137 | // ResourceLoader.GetString() requires '/' (not '.') as the separator for x:Uid property resources |
| 138 | { WslConfigEntry.ProcessorCount, "Settings_ProcCount/Header" }, |
| 139 | { WslConfigEntry.MemorySizeBytes, "Settings_MemorySize/Header" }, |
| 140 | { WslConfigEntry.SwapSizeBytes, "Settings_SwapSize/Header" }, |
| 141 | { WslConfigEntry.SwapFilePath, "Settings_SwapFilePath/Header" }, |
| 142 | { WslConfigEntry.VhdSizeBytes, "Settings_DefaultVHDSize/Header" }, |
| 143 | { WslConfigEntry.NetworkingMode, "Settings_NetworkingMode/Header" }, |
| 144 | { WslConfigEntry.FirewallEnabled, "Settings_HyperVFirewall/Header" }, |
| 145 | { WslConfigEntry.IgnoredPorts, "Settings_IgnoredPorts/Header" }, |
| 146 | { WslConfigEntry.LocalhostForwardingEnabled, "Settings_LocalhostForwarding/Header" }, |
| 147 | { WslConfigEntry.HostAddressLoopbackEnabled, "Settings_HostAddressLoopback/Header" }, |
| 148 | { WslConfigEntry.AutoProxyEnabled, "Settings_AutoProxy/Header" }, |
| 149 | { WslConfigEntry.InitialAutoProxyTimeout, "Settings_InitialAutoProxyTimeout/Header" }, |
| 150 | { WslConfigEntry.DNSProxyEnabled, "Settings_DNSProxy/Header" }, |
| 151 | { WslConfigEntry.DNSTunnelingEnabled, "Settings_DNSTunneling/Header" }, |
| 152 | { WslConfigEntry.BestEffortDNSParsingEnabled, "Settings_BestEffortDNS/Header" }, |
| 153 | { WslConfigEntry.AutoMemoryReclaim, "Settings_AutoMemoryReclaim/Header" }, |
| 154 | { WslConfigEntry.GUIApplicationsEnabled, "Settings_GUIApplications/Header" }, |
| 155 | { WslConfigEntry.NestedVirtualizationEnabled, "Settings_NestedVirtualization/Header" }, |
| 156 | { WslConfigEntry.SafeModeEnabled, "Settings_SafeMode/Header" }, |
| 157 | { WslConfigEntry.SparseVHDEnabled, "Settings_SparseVHD/Header" }, |
| 158 | { WslConfigEntry.VMIdleTimeout, "Settings_VMIdleTimeout/Header" }, |
| 159 | { WslConfigEntry.InstanceIdleTimeout, "Settings_InstanceIdleTimeout/Header" }, |
| 160 | { WslConfigEntry.DebugConsoleEnabled, "Settings_DebugConsole/Header" }, |
| 161 | { WslConfigEntry.HardwarePerformanceCountersEnabled, "Settings_HWPerfCounters/Header" }, |
| 162 | { WslConfigEntry.KernelPath, "Settings_CustomKernelPath/Header" }, |
| 163 | { WslConfigEntry.SystemDistroPath, "Settings_CustomSystemDistroPath/Header" }, |
| 164 | { WslConfigEntry.KernelModulesPath, "Settings_CustomKernelModulesPath/Header" }, |
| 165 | }; |
| 166 | |
| 167 | private static string FormatValue(WslConfigEntry entry, object value) |
| 168 | { |
| 169 | switch (entry.GetValueKind()) |
| 170 | { |
| 171 | case WslConfigValueKind.UInt64: |
| 172 | return string.Format("Settings_MegabyteStringFormat".GetLocalized(), (ulong)value / Constants.MB); |
| 173 | case WslConfigValueKind.Int32: |
| 174 | switch (entry) |
| 175 | { |
| 176 | case WslConfigEntry.InitialAutoProxyTimeout: |
| 177 | case WslConfigEntry.InstanceIdleTimeout: |
| 178 | case WslConfigEntry.VMIdleTimeout: |
| 179 | return string.Format("Settings_MillisecondsStringFormat".GetLocalized(), (int)value); |
| 180 | default: |
| 181 | return ((int)value).ToString(); |
| 182 | } |
| 183 | case WslConfigValueKind.String: |
| 184 | return (string?)value ?? string.Empty; |
| 185 | case WslConfigValueKind.NetworkingConfiguration: |
| 186 | return FormatEnum((NetworkingConfiguration)value); |
| 187 | case WslConfigValueKind.MemoryReclaimMode: |
| 188 | return FormatEnum((MemoryReclaimMode)value); |
| 189 | default: |
| 190 | return FormatBool((bool)value); |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | private static string FormatBool(bool value) |
| 195 | { |
| 196 | var localized = value |
| 197 | ? "Settings_BooleanTrueText".GetLocalized() |
| 198 | : "Settings_BooleanFalseText".GetLocalized(); |
| 199 | return string.IsNullOrEmpty(localized) |
| 200 | ? (value ? bool.TrueString : bool.FalseString) |
| 201 | : localized; |
| 202 | } |
| 203 | |
| 204 | private static string FormatEnum<TEnum>(TEnum value) where TEnum : struct, Enum |
| 205 | { |
| 206 | // Try resource lookup first using the pattern "Settings_{EnumTypeName}_{EnumValue}". |
| 207 | // GetLocalized throws COMException when the key doesn't exist, so fall back to the raw name. |
| 208 | try |
| 209 | { |
| 210 | var resourceKey = $"Settings_{typeof(TEnum).Name}_{value}"; |
| 211 | var localized = resourceKey.GetLocalized(); |
| 212 | if (!string.IsNullOrEmpty(localized) && localized != resourceKey) |
| 213 | { |
| 214 | return localized; |
| 215 | } |
| 216 | } |
| 217 | catch (COMException) |
| 218 | { |
| 219 | } |
| 220 | |
| 221 | return value.ToString(); |
| 222 | } |
| 223 | } |