master
cs 223 lines 9.81 KB
Raw
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 }