DeviceDetailsWindow.xaml.cs 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225
  1. using System.Globalization;
  2. using System.Collections.ObjectModel;
  3. using System.ComponentModel;
  4. using System.Runtime.CompilerServices;
  5. using System.Windows;
  6. using System.Windows.Controls;
  7. using System.Windows.Input;
  8. using System.Windows.Media;
  9. using System.Windows.Media.Animation;
  10. using NetworkTool.Client.Models;
  11. using NetworkTool.Client.Services;
  12. namespace NetworkTool.Client;
  13. public partial class DeviceDetailsWindow : Window
  14. {
  15. private const int ApplyConfirmationTimeoutSeconds = 20;
  16. private readonly ServerApiService _serverApiService = new();
  17. private readonly ObservableCollection<InterfaceEditor> _interfaces = [];
  18. private readonly string _baseAddress;
  19. private readonly string _remoteHost;
  20. private readonly string _localIPv4;
  21. private readonly string _password;
  22. private bool _configValidated;
  23. private bool _configDirty;
  24. private bool _isBusy;
  25. private bool _suppressConfigChangeHandling;
  26. private CancellationTokenSource? _statusMessageCts;
  27. public DeviceDetailsWindow(string baseAddress, string localIPv4, string password)
  28. {
  29. InitializeComponent();
  30. InterfacesItemsControl.ItemsSource = _interfaces;
  31. _baseAddress = baseAddress;
  32. _remoteHost = GetRemoteHost(baseAddress);
  33. _localIPv4 = localIPv4;
  34. _password = password;
  35. UpdateWindowTitle();
  36. Loaded += DeviceDetailsWindow_OnLoaded;
  37. Closing += DeviceDetailsWindow_OnClosing;
  38. }
  39. private async void DeviceDetailsWindow_OnLoaded(object sender, RoutedEventArgs e)
  40. {
  41. try
  42. {
  43. await LoadRemoteDetailsAsync();
  44. UpdateButtonStates();
  45. }
  46. catch (Exception ex)
  47. {
  48. ShowStatusMessage($"读取设备信息失败:{ex.Message}");
  49. SetBusyState(false);
  50. }
  51. }
  52. private async Task LoadRemoteDetailsAsync()
  53. {
  54. ClearDetails();
  55. var device = await _serverApiService.GetDeviceInfoAsync(_baseAddress, _password, _localIPv4);
  56. if (device is not null)
  57. {
  58. UpdateWindowTitle(device.Hostname, device.ServerVersion);
  59. }
  60. var interfaces = await _serverApiService.GetInterfacesAsync(_baseAddress, _password, _localIPv4);
  61. if (interfaces is null)
  62. {
  63. ShowStatusMessage("设备已连接,但暂时无法读取 Linux 接口列表。");
  64. return;
  65. }
  66. ShowStatusMessage($"当前管理接口:{interfaces.ManagementInterface}。正在读取全部接口配置。");
  67. foreach (var info in interfaces.Interfaces)
  68. {
  69. var editor = new InterfaceEditor(info);
  70. _interfaces.Add(editor);
  71. await LoadRemoteInterfaceConfigAsync(editor);
  72. }
  73. _configValidated = false;
  74. _configDirty = false;
  75. ShowStatusMessage("已读取全部接口配置。");
  76. }
  77. private void ClearDetails()
  78. {
  79. UpdateWindowTitle();
  80. _interfaces.Clear();
  81. _configValidated = false;
  82. _configDirty = false;
  83. }
  84. private void UpdateWindowTitle(string? hostname = null, string? serverVersion = null)
  85. {
  86. var hostPart = string.IsNullOrWhiteSpace(hostname) ? _remoteHost : $"{hostname} ({_remoteHost})";
  87. var versionPart = string.IsNullOrWhiteSpace(serverVersion) ? string.Empty : $" - Server {serverVersion}";
  88. Title = string.IsNullOrWhiteSpace(hostPart) ? $"设备信息与接口配置{versionPart}" : $"设备信息与接口配置 - {hostPart}{versionPart}";
  89. }
  90. private static string GetRemoteHost(string baseAddress)
  91. {
  92. return Uri.TryCreate(baseAddress, UriKind.Absolute, out var uri) ? uri.Host : baseAddress;
  93. }
  94. private async Task LoadRemoteInterfaceConfigAsync(InterfaceEditor editor, bool useBusyState = false)
  95. {
  96. if (useBusyState)
  97. {
  98. SetBusyState(true, "正在读取 Linux 端 IP 配置...");
  99. }
  100. try
  101. {
  102. var result = await _serverApiService.GetInterfaceConfigAsync(_baseAddress, _password, _localIPv4, editor.SystemName);
  103. if (!result.Success || result.Data is null)
  104. {
  105. ShowStatusMessage($"读取目标接口 {editor.SystemName} 配置失败:{result.Message}");
  106. return;
  107. }
  108. var config = result.Data;
  109. _suppressConfigChangeHandling = true;
  110. editor.Dhcp4 = config.Dhcp4;
  111. editor.Addresses.Clear();
  112. foreach (var address in config.EffectiveAddresses)
  113. {
  114. editor.Addresses.Add(new EditableAddress(editor) { IP = address.IP, Mask = PrefixToMask(address.Prefix) });
  115. }
  116. editor.Routes.Clear();
  117. editor.DefaultGatewayEnabled = false;
  118. editor.DefaultGateway = string.Empty;
  119. foreach (var route in config.EffectiveRoutes)
  120. {
  121. if (route.To.Equals("default", StringComparison.OrdinalIgnoreCase))
  122. {
  123. editor.DefaultGatewayEnabled = true;
  124. editor.DefaultGateway = route.Via;
  125. }
  126. else
  127. {
  128. editor.Routes.Add(CreateEditableRoute(editor, route));
  129. }
  130. }
  131. editor.CustomRoutesEnabled = editor.Routes.Count > 0;
  132. editor.Dns.Clear();
  133. if (config.Dns is not null)
  134. {
  135. foreach (var dns in config.Dns)
  136. {
  137. editor.Dns.Add(new EditableDns(editor) { Address = dns });
  138. }
  139. }
  140. _suppressConfigChangeHandling = false;
  141. UpdateButtonStates();
  142. }
  143. finally
  144. {
  145. _suppressConfigChangeHandling = false;
  146. if (useBusyState)
  147. {
  148. SetBusyState(false);
  149. }
  150. }
  151. }
  152. private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
  153. {
  154. if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未应用,刷新会丢失未应用内容。是否继续刷新?", "确认刷新配置"))
  155. {
  156. return;
  157. }
  158. SetBusyState(true, "正在刷新全部接口配置...");
  159. try
  160. {
  161. foreach (var editor in _interfaces)
  162. {
  163. await LoadRemoteInterfaceConfigAsync(editor);
  164. }
  165. _configValidated = false;
  166. _configDirty = false;
  167. ShowStatusMessage("已刷新全部接口配置。");
  168. }
  169. finally
  170. {
  171. SetBusyState(false);
  172. }
  173. }
  174. private void DeviceDetailsWindow_OnClosing(object? sender, CancelEventArgs e)
  175. {
  176. if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未应用。是否关闭窗口?", "确认关闭窗口"))
  177. {
  178. e.Cancel = true;
  179. }
  180. }
  181. private bool ConfirmDiscardPendingChanges(string message, string title)
  182. {
  183. if (!_configDirty)
  184. {
  185. return true;
  186. }
  187. return MessageBox.Show(this, message, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning) == MessageBoxResult.OK;
  188. }
  189. private async void ValidateConfigButton_OnClick(object sender, RoutedEventArgs e)
  190. {
  191. var request = BuildConfigRequests();
  192. if (request is null)
  193. {
  194. return;
  195. }
  196. SetBusyState(true, "正在校验配置,请稍候...");
  197. try
  198. {
  199. var result = await _serverApiService.ValidateInterfaceConfigsAsync(_baseAddress, _password, _localIPv4, request);
  200. _configValidated = result.Success && result.Data?.Valid == true;
  201. if (result.Data is not null)
  202. {
  203. var warnings = result.Data.Warnings.Count > 0 ? $" 警告:{string.Join(";", result.Data.Warnings)}" : string.Empty;
  204. var errors = result.Data.Errors.Count > 0 ? $" 错误:{string.Join(";", result.Data.Errors)}" : string.Empty;
  205. ShowStatusMessage(_configValidated ? $"全部接口校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}");
  206. }
  207. else
  208. {
  209. ShowStatusMessage($"校验失败:{result.Message}");
  210. }
  211. UpdateButtonStates();
  212. }
  213. finally
  214. {
  215. SetBusyState(false);
  216. }
  217. }
  218. private async void ApplyConfigButton_OnClick(object sender, RoutedEventArgs e)
  219. {
  220. var request = BuildConfigRequests();
  221. if (request is null)
  222. {
  223. return;
  224. }
  225. var confirmMessage = "将要一次性应用以下接口配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。";
  226. if (MessageBox.Show(this, confirmMessage, "确认应用配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
  227. {
  228. return;
  229. }
  230. SetBusyState(true, "正在提交并应用配置,请稍候...");
  231. try
  232. {
  233. var applyResult = await _serverApiService.ApplyInterfaceConfigsAsync(_baseAddress, _password, _localIPv4, request);
  234. if (!applyResult.Success || applyResult.Data is null)
  235. {
  236. ShowStatusMessage($"提交配置任务失败:{applyResult.Message}");
  237. return;
  238. }
  239. ShowStatusMessage("配置任务已提交,正在应用并等待连通确认...");
  240. await PollTaskAsync(applyResult.Data.TaskId);
  241. }
  242. finally
  243. {
  244. SetBusyState(false);
  245. }
  246. }
  247. private async Task PollTaskAsync(string taskId)
  248. {
  249. var transientFailureCount = 0;
  250. var confirmationRequested = false;
  251. for (var i = 0; i < 20; i++)
  252. {
  253. await Task.Delay(1000);
  254. var result = await _serverApiService.GetTaskAsync(_baseAddress, _password, _localIPv4, taskId);
  255. if (!result.Success || result.Data is null)
  256. {
  257. if (result.StatusCode is null)
  258. {
  259. transientFailureCount++;
  260. ShowStatusMessage($"设备连接短暂中断,正在重试({transientFailureCount})。");
  261. continue;
  262. }
  263. ShowStatusMessage($"读取任务状态失败:{result.Message}");
  264. return;
  265. }
  266. transientFailureCount = 0;
  267. var task = result.Data;
  268. ShowStatusMessage(FormatTaskStatusMessage(task));
  269. if (task.Status == "running" && task.Step == "confirming" && !confirmationRequested)
  270. {
  271. confirmationRequested = true;
  272. var confirm = ShowApplyConfirmationDialog(ApplyConfirmationTimeoutSeconds);
  273. if (confirm)
  274. {
  275. var confirmResult = await _serverApiService.ConfirmApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
  276. ShowStatusMessage(confirmResult.Success ? "已发送保留配置确认。" : $"发送确认失败:{confirmResult.Message}");
  277. }
  278. else
  279. {
  280. var cancelResult = await _serverApiService.CancelApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
  281. ShowStatusMessage(cancelResult.Success ? "已取消保留配置,正在回滚。" : $"发送取消失败:{cancelResult.Message}");
  282. }
  283. }
  284. if (task.Status is "success" or "failed" or "rolled_back")
  285. {
  286. ShowTaskCompletionDialog(task);
  287. foreach (var editor in _interfaces)
  288. {
  289. await LoadRemoteInterfaceConfigAsync(editor);
  290. }
  291. return;
  292. }
  293. }
  294. ShowStatusMessage($"任务 {taskId} 轮询超时,请稍后手动刷新。");
  295. }
  296. private bool ShowApplyConfirmationDialog(int timeoutSeconds)
  297. {
  298. var remaining = timeoutSeconds;
  299. var result = false;
  300. var messageTextBlock = new TextBlock
  301. {
  302. Width = 420,
  303. TextWrapping = TextWrapping.Wrap,
  304. FontSize = 13,
  305. Foreground = Brushes.Black,
  306. };
  307. var confirmButton = new Button
  308. {
  309. MinWidth = 88,
  310. MinHeight = 32,
  311. Margin = new Thickness(0, 0, 10, 0),
  312. Content = "保留",
  313. IsDefault = true,
  314. };
  315. confirmButton.Style = (Style)FindResource("PrimaryButtonStyle");
  316. var cancelButton = new Button
  317. {
  318. MinWidth = 88,
  319. MinHeight = 32,
  320. Content = "回滚",
  321. IsCancel = true,
  322. };
  323. var dialog = new Window
  324. {
  325. Title = "确认保留网络配置",
  326. Owner = this,
  327. WindowStartupLocation = WindowStartupLocation.CenterOwner,
  328. ResizeMode = ResizeMode.NoResize,
  329. SizeToContent = SizeToContent.WidthAndHeight,
  330. Content = new StackPanel
  331. {
  332. Margin = new Thickness(18),
  333. Children =
  334. {
  335. messageTextBlock,
  336. new StackPanel
  337. {
  338. Margin = new Thickness(0, 18, 0, 0),
  339. HorizontalAlignment = HorizontalAlignment.Right,
  340. Orientation = Orientation.Horizontal,
  341. Children = { confirmButton, cancelButton },
  342. },
  343. },
  344. },
  345. };
  346. void UpdateMessage()
  347. {
  348. messageTextBlock.Text = $"当前客户端仍可连接到设备。是否确认保留这次网络配置?\n\n剩余 {remaining} 秒;超时或取消时,Linux 端会自动回滚。";
  349. }
  350. var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
  351. timer.Tick += (_, _) =>
  352. {
  353. remaining--;
  354. if (remaining <= 0)
  355. {
  356. timer.Stop();
  357. dialog.DialogResult = false;
  358. dialog.Close();
  359. return;
  360. }
  361. UpdateMessage();
  362. };
  363. confirmButton.Click += (_, _) =>
  364. {
  365. result = true;
  366. dialog.DialogResult = true;
  367. dialog.Close();
  368. };
  369. cancelButton.Click += (_, _) =>
  370. {
  371. dialog.DialogResult = false;
  372. dialog.Close();
  373. };
  374. dialog.Closed += (_, _) => timer.Stop();
  375. UpdateMessage();
  376. timer.Start();
  377. dialog.ShowDialog();
  378. return result;
  379. }
  380. private async void RebootButton_OnClick(object sender, RoutedEventArgs e)
  381. {
  382. await ExecuteSystemActionAsync(
  383. "重启设备",
  384. "设备将立即重启,当前窗口和连接可能马上中断。是否继续?",
  385. () => _serverApiService.RebootAsync(_baseAddress, _password, _localIPv4));
  386. }
  387. private async void ShutdownButton_OnClick(object sender, RoutedEventArgs e)
  388. {
  389. await ExecuteSystemActionAsync(
  390. "关闭设备",
  391. "设备将立即关机,当前窗口和连接可能马上中断。是否继续?",
  392. () => _serverApiService.ShutdownAsync(_baseAddress, _password, _localIPv4));
  393. }
  394. private async Task ExecuteSystemActionAsync(string title, string confirmMessage, Func<Task<ApiCallResult<RemoteSystemActionResponse>>> action)
  395. {
  396. if (MessageBox.Show(this, confirmMessage, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning) != MessageBoxResult.OK)
  397. {
  398. return;
  399. }
  400. var result = await action();
  401. if (!result.Success || result.Data is null)
  402. {
  403. ShowStatusMessage($"{title}失败:{result.Message}");
  404. return;
  405. }
  406. ShowStatusMessage($"{title}任务已提交:{result.Data.TaskId}。命令已发出,设备可能立即断开。");
  407. }
  408. private RemoteInterfaceConfig[]? BuildConfigRequests()
  409. {
  410. CommitConfigEdits();
  411. var result = new List<RemoteInterfaceConfig>();
  412. foreach (var editor in _interfaces)
  413. {
  414. var request = BuildConfigRequest(editor);
  415. if (request is null)
  416. {
  417. return null;
  418. }
  419. result.Add(request);
  420. }
  421. if (result.Count == 0)
  422. {
  423. ShowStatusMessage("接口配置不能为空。");
  424. return null;
  425. }
  426. return result.ToArray();
  427. }
  428. private RemoteInterfaceConfig? BuildConfigRequest(InterfaceEditor editor)
  429. {
  430. var dhcp4 = editor.Dhcp4;
  431. var addresses = Array.Empty<RemoteInterfaceAddressConfig>();
  432. var routes = Array.Empty<RemoteInterfaceRouteConfig>();
  433. if (!dhcp4)
  434. {
  435. if (editor.Addresses.All(item => string.IsNullOrWhiteSpace(item.IP) && string.IsNullOrWhiteSpace(item.Mask)))
  436. {
  437. ShowStatusMessage($"{editor.SystemName}:IP 地址不能为空,至少需要填写一行地址。");
  438. return null;
  439. }
  440. if (!TryBuildAddresses(editor, out addresses, out var addressError))
  441. {
  442. ShowStatusMessage($"{editor.SystemName}:{addressError}");
  443. return null;
  444. }
  445. if (!TryBuildRoutes(editor, out routes, out var routeError))
  446. {
  447. ShowStatusMessage($"{editor.SystemName}:{routeError}");
  448. return null;
  449. }
  450. }
  451. var dns = editor.Dns.Select(item => item.Address.Trim()).Where(item => item != string.Empty).ToArray();
  452. return new RemoteInterfaceConfig
  453. {
  454. Interface = editor.SystemName,
  455. Dhcp4 = dhcp4,
  456. Addresses = dhcp4 ? Array.Empty<RemoteInterfaceAddressConfig>() : addresses,
  457. Routes = dhcp4 ? Array.Empty<RemoteInterfaceRouteConfig>() : routes,
  458. Dns = dns,
  459. };
  460. }
  461. private void CommitConfigEdits()
  462. {
  463. CommitDataGridEdits(InterfacesItemsControl);
  464. }
  465. private static void CommitDataGridEdits(DependencyObject root)
  466. {
  467. for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
  468. {
  469. var child = VisualTreeHelper.GetChild(root, i);
  470. if (child is DataGrid dataGrid)
  471. {
  472. dataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
  473. dataGrid.CommitEdit(DataGridEditingUnit.Row, true);
  474. }
  475. CommitDataGridEdits(child);
  476. }
  477. }
  478. private static string FormatCurrentIp(RemoteInterfaceConfig config)
  479. {
  480. if (config.EffectiveAddresses.Count == 0)
  481. {
  482. return config.Dhcp4 ? "DHCP 自动获取,暂无 IPv4" : "无";
  483. }
  484. var text = FormatAddresses(config.EffectiveAddresses);
  485. return config.Dhcp4 ? $"{text} (DHCP)" : text;
  486. }
  487. private static string FormatAddresses(IReadOnlyList<RemoteInterfaceAddressConfig> addresses)
  488. {
  489. return addresses.Count == 0 ? "无" : string.Join(Environment.NewLine, addresses.Select(item => $"{item.IP}/{item.Prefix}"));
  490. }
  491. private static string FormatRoutes(IReadOnlyList<RemoteInterfaceRouteConfig> routes)
  492. {
  493. return routes.Count == 0 ? "无" : string.Join(Environment.NewLine, routes.Select(item => $"{item.To} via {item.Via}"));
  494. }
  495. private static string FormatConfigSummary(IReadOnlyList<RemoteInterfaceConfig> configs)
  496. {
  497. return string.Join(Environment.NewLine + Environment.NewLine, configs.Select(item =>
  498. $"接口:{item.Interface}\n" +
  499. $"模式:{(item.Dhcp4 ? "DHCP 自动获取" : "静态 IPv4")}\n" +
  500. $"IP:{(item.Dhcp4 ? "自动获取" : FormatAddresses(item.Addresses))}\n" +
  501. $"路由:{(item.Dhcp4 ? "自动获取" : FormatRoutes(item.Routes))}\n" +
  502. $"DNS:{(item.Dns.Count == 0 ? "无" : string.Join(", ", item.Dns))}"));
  503. }
  504. private static EditableRoute CreateEditableRoute(InterfaceEditor owner, RemoteInterfaceRouteConfig route)
  505. {
  506. var to = route.To.Trim();
  507. var mask = string.Empty;
  508. if (to.Contains('/'))
  509. {
  510. var parts = to.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  511. if (parts.Length == 2 && int.TryParse(parts[1], out var prefix) && prefix >= 0 && prefix <= 32)
  512. {
  513. to = parts[0];
  514. mask = PrefixToMask(prefix);
  515. }
  516. }
  517. return new EditableRoute(owner) { To = to, Mask = mask, Via = route.Via };
  518. }
  519. private bool TryBuildAddresses(InterfaceEditor editor, out RemoteInterfaceAddressConfig[] addresses, out string error)
  520. {
  521. var result = new List<RemoteInterfaceAddressConfig>();
  522. foreach (var row in editor.Addresses)
  523. {
  524. var ip = row.IP.Trim();
  525. var maskText = row.Mask.Trim();
  526. if (ip == string.Empty && maskText == string.Empty)
  527. {
  528. continue;
  529. }
  530. if (ip.Contains('/'))
  531. {
  532. var parts = ip.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  533. if (parts.Length != 2 || !int.TryParse(parts[1], out var cidrPrefix) || cidrPrefix < 0 || cidrPrefix > 32)
  534. {
  535. addresses = [];
  536. error = $"地址格式不正确:{ip}";
  537. return false;
  538. }
  539. row.IP = parts[0];
  540. row.Mask = PrefixToMask(cidrPrefix);
  541. result.Add(new RemoteInterfaceAddressConfig { IP = parts[0], Prefix = cidrPrefix });
  542. continue;
  543. }
  544. if (ip == string.Empty || maskText == string.Empty)
  545. {
  546. addresses = [];
  547. error = "IP 地址和子网掩码都需要填写。";
  548. return false;
  549. }
  550. if (!TryMaskOrPrefixToPrefix(maskText, out var prefix))
  551. {
  552. addresses = [];
  553. error = $"子网掩码格式不正确:{ip} {maskText}";
  554. return false;
  555. }
  556. row.Mask = PrefixToMask(prefix);
  557. result.Add(new RemoteInterfaceAddressConfig { IP = ip, Prefix = prefix });
  558. }
  559. addresses = result.ToArray();
  560. error = string.Empty;
  561. return addresses.Length > 0;
  562. }
  563. private bool TryBuildRoutes(InterfaceEditor editor, out RemoteInterfaceRouteConfig[] routes, out string error)
  564. {
  565. var result = new List<RemoteInterfaceRouteConfig>();
  566. if (editor.DefaultGatewayEnabled)
  567. {
  568. var gateway = editor.DefaultGateway.Trim();
  569. if (gateway == string.Empty)
  570. {
  571. routes = [];
  572. error = "启用默认网关时,网关地址不能为空。";
  573. return false;
  574. }
  575. result.Add(new RemoteInterfaceRouteConfig { To = "default", Via = gateway });
  576. }
  577. if (editor.CustomRoutesEnabled)
  578. {
  579. foreach (var row in editor.Routes)
  580. {
  581. var to = row.To.Trim();
  582. var maskText = row.Mask.Trim();
  583. var via = row.Via.Trim();
  584. if (to == string.Empty && maskText == string.Empty && via == string.Empty)
  585. {
  586. continue;
  587. }
  588. if (to.Contains('/'))
  589. {
  590. var parts = to.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  591. if (parts.Length != 2 || !int.TryParse(parts[1], out var cidrPrefix) || cidrPrefix < 0 || cidrPrefix > 32)
  592. {
  593. routes = [];
  594. error = $"自定义路由目标网段格式不正确:{to}";
  595. return false;
  596. }
  597. to = parts[0];
  598. maskText = PrefixToMask(cidrPrefix);
  599. row.To = to;
  600. row.Mask = maskText;
  601. }
  602. if (to == string.Empty || maskText == string.Empty || via == string.Empty)
  603. {
  604. routes = [];
  605. error = "自定义路由的目标网段、子网掩码和网关地址都需要填写。";
  606. return false;
  607. }
  608. if (!TryMaskOrPrefixToPrefix(maskText, out var prefix))
  609. {
  610. routes = [];
  611. error = $"自定义路由子网掩码格式不正确:{to} {maskText}";
  612. return false;
  613. }
  614. row.Mask = PrefixToMask(prefix);
  615. result.Add(new RemoteInterfaceRouteConfig { To = $"{to}/{prefix}", Via = via });
  616. }
  617. }
  618. routes = result.ToArray();
  619. error = string.Empty;
  620. return true;
  621. }
  622. private static bool TryParseAddresses(string text, out RemoteInterfaceAddressConfig[] addresses, out string error)
  623. {
  624. var result = new List<RemoteInterfaceAddressConfig>();
  625. foreach (var line in ParseListText(text))
  626. {
  627. var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  628. if (parts.Length == 1 && line.Contains('/'))
  629. {
  630. var cidrParts = line.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  631. if (cidrParts.Length != 2 || !int.TryParse(cidrParts[1], out var prefix) || prefix < 0 || prefix > 32)
  632. {
  633. addresses = [];
  634. error = $"地址格式不正确:{line}";
  635. return false;
  636. }
  637. result.Add(new RemoteInterfaceAddressConfig { IP = cidrParts[0], Prefix = prefix });
  638. continue;
  639. }
  640. if (parts.Length != 2)
  641. {
  642. addresses = [];
  643. error = $"地址格式不正确:{line}";
  644. return false;
  645. }
  646. if (!TryMaskOrPrefixToPrefix(parts[1], out var parsedPrefix))
  647. {
  648. addresses = [];
  649. error = $"子网掩码或前缀格式不正确:{line}";
  650. return false;
  651. }
  652. result.Add(new RemoteInterfaceAddressConfig { IP = parts[0], Prefix = parsedPrefix });
  653. }
  654. addresses = result.ToArray();
  655. error = string.Empty;
  656. return addresses.Length > 0;
  657. }
  658. private static bool TryParseRoutes(string text, out RemoteInterfaceRouteConfig[] routes, out string error)
  659. {
  660. var result = new List<RemoteInterfaceRouteConfig>();
  661. foreach (var line in ParseListText(text))
  662. {
  663. var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  664. if (parts.Length == 1)
  665. {
  666. result.Add(new RemoteInterfaceRouteConfig { To = "default", Via = parts[0] });
  667. continue;
  668. }
  669. if (parts.Length == 3 && parts[1].Equals("via", StringComparison.OrdinalIgnoreCase))
  670. {
  671. result.Add(new RemoteInterfaceRouteConfig { To = parts[0], Via = parts[2] });
  672. continue;
  673. }
  674. routes = [];
  675. error = $"路由格式不正确:{line}";
  676. return false;
  677. }
  678. routes = result.ToArray();
  679. error = string.Empty;
  680. return true;
  681. }
  682. private static string[] ParseListText(string text)
  683. {
  684. return text.Split(['\r', '\n', ',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  685. }
  686. private static bool TryMaskOrPrefixToPrefix(string text, out int prefix)
  687. {
  688. if (int.TryParse(text, out prefix) && prefix >= 0 && prefix <= 32)
  689. {
  690. return true;
  691. }
  692. return TryMaskToPrefix(text, out prefix);
  693. }
  694. private static string PrefixToMask(int prefix)
  695. {
  696. if (prefix < 0 || prefix > 32)
  697. {
  698. return string.Empty;
  699. }
  700. var mask = prefix == 0 ? 0u : uint.MaxValue << (32 - prefix);
  701. return string.Join('.', new[] { (mask >> 24) & 255, (mask >> 16) & 255, (mask >> 8) & 255, mask & 255 });
  702. }
  703. private static bool TryMaskToPrefix(string maskText, out int prefix)
  704. {
  705. prefix = 0;
  706. if (string.IsNullOrWhiteSpace(maskText))
  707. {
  708. return false;
  709. }
  710. var parts = maskText.Trim().Split('.');
  711. if (parts.Length != 4)
  712. {
  713. return false;
  714. }
  715. uint mask = 0;
  716. foreach (var part in parts)
  717. {
  718. if (!byte.TryParse(part, out var octet))
  719. {
  720. return false;
  721. }
  722. mask = (mask << 8) | octet;
  723. }
  724. var seenZero = false;
  725. for (var i = 31; i >= 0; i--)
  726. {
  727. var bit = (mask & (1u << i)) != 0;
  728. if (bit && seenZero)
  729. {
  730. return false;
  731. }
  732. if (bit)
  733. {
  734. prefix++;
  735. }
  736. else
  737. {
  738. seenZero = true;
  739. }
  740. }
  741. return true;
  742. }
  743. private void ConfigInputChanged_OnChanged(object sender, TextChangedEventArgs e)
  744. {
  745. if (_suppressConfigChangeHandling)
  746. {
  747. return;
  748. }
  749. _configValidated = false;
  750. ShowStatusMessage("配置内容已变更,请重新点击“2. 校验配置”。");
  751. UpdateButtonStates();
  752. }
  753. private void ConfigModeChanged_OnChanged(object sender, RoutedEventArgs e)
  754. {
  755. if (_suppressConfigChangeHandling)
  756. {
  757. UpdateButtonStates();
  758. return;
  759. }
  760. _configValidated = false;
  761. _configDirty = true;
  762. ShowStatusMessage("配置模式已变更,请重新点击“2. 校验配置”。");
  763. UpdateButtonStates();
  764. }
  765. private void GatewayOrRouteModeChanged_OnChanged(object sender, RoutedEventArgs e)
  766. {
  767. if (_suppressConfigChangeHandling)
  768. {
  769. UpdateButtonStates();
  770. return;
  771. }
  772. _configValidated = false;
  773. _configDirty = true;
  774. ShowStatusMessage("配置内容已变更,请重新点击“2. 校验配置”。");
  775. UpdateButtonStates();
  776. }
  777. private void ConfigGrid_OnCellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
  778. {
  779. if (e.Row.Item is EditableAddress address)
  780. {
  781. NormalizeAddressRow(address);
  782. }
  783. else if (e.Row.Item is EditableRoute route)
  784. {
  785. NormalizeRouteRow(route);
  786. }
  787. MarkConfigChanged("配置内容已变更,请重新点击“2. 校验配置”。");
  788. }
  789. private void DataGrid_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
  790. {
  791. if (sender is not DataGrid dataGrid)
  792. {
  793. return;
  794. }
  795. e.Handled = true;
  796. var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
  797. {
  798. RoutedEvent = MouseWheelEvent,
  799. Source = dataGrid,
  800. };
  801. ContentScrollViewer.RaiseEvent(eventArg);
  802. }
  803. private void AddAddressButton_OnClick(object sender, RoutedEventArgs e)
  804. {
  805. if ((sender as FrameworkElement)?.DataContext is not InterfaceEditor editor)
  806. {
  807. return;
  808. }
  809. editor.Addresses.Add(new EditableAddress(editor) { Mask = "255.255.255.0" });
  810. MarkConfigChanged("已添加 IP 地址,请填写后重新校验配置。");
  811. }
  812. private void AddRouteButton_OnClick(object sender, RoutedEventArgs e)
  813. {
  814. if ((sender as FrameworkElement)?.DataContext is not InterfaceEditor editor)
  815. {
  816. return;
  817. }
  818. editor.Routes.Add(new EditableRoute(editor));
  819. MarkConfigChanged("已添加路由,请填写后重新校验配置。");
  820. }
  821. private void AddDnsButton_OnClick(object sender, RoutedEventArgs e)
  822. {
  823. if ((sender as FrameworkElement)?.DataContext is not InterfaceEditor editor)
  824. {
  825. return;
  826. }
  827. editor.Dns.Add(new EditableDns(editor));
  828. MarkConfigChanged("已添加 DNS,请填写后重新校验配置。");
  829. }
  830. private void DeleteAddressButton_OnClick(object sender, RoutedEventArgs e)
  831. {
  832. if ((sender as FrameworkElement)?.DataContext is not EditableAddress address)
  833. {
  834. return;
  835. }
  836. address.Owner.Addresses.Remove(address);
  837. MarkConfigChanged("已删除 IP 地址,请重新校验配置。");
  838. }
  839. private void DeleteRouteButton_OnClick(object sender, RoutedEventArgs e)
  840. {
  841. if ((sender as FrameworkElement)?.DataContext is EditableRoute route)
  842. {
  843. route.Owner.Routes.Remove(route);
  844. MarkConfigChanged("已删除路由,请重新校验配置。");
  845. }
  846. }
  847. private void DeleteDnsButton_OnClick(object sender, RoutedEventArgs e)
  848. {
  849. if ((sender as FrameworkElement)?.DataContext is EditableDns dns)
  850. {
  851. dns.Owner.Dns.Remove(dns);
  852. MarkConfigChanged("已删除 DNS,请重新校验配置。");
  853. }
  854. }
  855. private void MarkConfigChanged(string message)
  856. {
  857. if (_suppressConfigChangeHandling)
  858. {
  859. return;
  860. }
  861. _configValidated = false;
  862. _configDirty = true;
  863. ShowStatusMessage(message);
  864. UpdateButtonStates();
  865. }
  866. private static void NormalizeAddressRow(EditableAddress row)
  867. {
  868. var ip = row.IP.Trim();
  869. if (!ip.Contains('/'))
  870. {
  871. return;
  872. }
  873. var parts = ip.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  874. if (parts.Length == 2 && int.TryParse(parts[1], out var prefix) && prefix >= 0 && prefix <= 32)
  875. {
  876. row.IP = parts[0];
  877. row.Mask = PrefixToMask(prefix);
  878. }
  879. }
  880. private static void NormalizeRouteRow(EditableRoute row)
  881. {
  882. var to = row.To.Trim();
  883. if (!to.Contains('/'))
  884. {
  885. return;
  886. }
  887. var parts = to.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  888. if (parts.Length == 2 && int.TryParse(parts[1], out var prefix) && prefix >= 0 && prefix <= 32)
  889. {
  890. row.To = parts[0];
  891. row.Mask = PrefixToMask(prefix);
  892. }
  893. }
  894. private void ShowStatusMessage(string message)
  895. {
  896. ApplyStatusMessageStyle(message);
  897. StatusMessageTextBlock.Text = message;
  898. StatusMessageBorder.Opacity = 0;
  899. StatusMessageBorder.Visibility = Visibility.Visible;
  900. StatusMessageBorder.BeginAnimation(OpacityProperty, new DoubleAnimation(1, TimeSpan.FromMilliseconds(160)));
  901. _statusMessageCts?.Cancel();
  902. _statusMessageCts = new CancellationTokenSource();
  903. _ = HideStatusMessageAsync(_statusMessageCts.Token);
  904. }
  905. private async Task HideStatusMessageAsync(CancellationToken cancellationToken)
  906. {
  907. try
  908. {
  909. await Task.Delay(3000, cancellationToken);
  910. await Dispatcher.InvokeAsync(() =>
  911. {
  912. var animation = new DoubleAnimation(0, TimeSpan.FromMilliseconds(200));
  913. animation.Completed += (_, _) =>
  914. {
  915. if (!cancellationToken.IsCancellationRequested)
  916. {
  917. StatusMessageBorder.Visibility = Visibility.Collapsed;
  918. }
  919. };
  920. StatusMessageBorder.BeginAnimation(OpacityProperty, animation);
  921. });
  922. }
  923. catch (TaskCanceledException)
  924. {
  925. }
  926. }
  927. private void ApplyStatusMessageStyle(string message)
  928. {
  929. var (background, foreground) = GetStatusMessageBrushes(message);
  930. StatusMessageBorder.Background = background;
  931. StatusMessageTextBlock.Foreground = foreground;
  932. }
  933. private static (Brush Background, Brush Foreground) GetStatusMessageBrushes(string message)
  934. {
  935. if (ContainsAny(message, "失败", "错误", "拒绝", "超时", "不能为空", "不正确", "无法"))
  936. {
  937. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B91C1C")), Brushes.White);
  938. }
  939. if (ContainsAny(message, "未发现", "请", "重试", "警告", "需要"))
  940. {
  941. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#C2410C")), Brushes.White);
  942. }
  943. if (ContainsAny(message, "成功", "已切换", "已刷新", "已读取", "已加载", "已发现", "已提交", "已回填"))
  944. {
  945. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#047857")), Brushes.White);
  946. }
  947. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#111827")), Brushes.White);
  948. }
  949. private static bool ContainsAny(string message, params string[] markers)
  950. {
  951. return markers.Any(marker => message.Contains(marker, StringComparison.Ordinal));
  952. }
  953. private static string FormatTaskStatusMessage(RemoteTaskResult task)
  954. {
  955. return task.Status switch
  956. {
  957. "success" => string.IsNullOrWhiteSpace(task.Detail) ? "配置已成功应用。" : task.Detail,
  958. "failed" => string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败。" : task.Detail,
  959. "rolled_back" => string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败,已自动回滚。" : task.Detail,
  960. _ => task.Step switch
  961. {
  962. "validating" => "正在校验配置...",
  963. "writing_netplan" => "正在写入 Linux 网络配置...",
  964. "applying" => "正在应用 Linux 网络配置...",
  965. "confirming" => string.IsNullOrWhiteSpace(task.Detail) ? "等待确认保留配置..." : task.Detail,
  966. "rolling_back" => "配置应用失败,正在自动回滚...",
  967. _ => string.IsNullOrWhiteSpace(task.Detail) ? "正在处理,请稍候..." : task.Detail,
  968. }
  969. };
  970. }
  971. private void ShowTaskCompletionDialog(RemoteTaskResult task)
  972. {
  973. var message = FormatTaskStatusMessage(task);
  974. var title = task.Status == "success" ? "应用配置成功" : "应用配置失败";
  975. var image = task.Status == "success" ? MessageBoxImage.Information : MessageBoxImage.Warning;
  976. MessageBox.Show(this, message, title, MessageBoxButton.OK, image);
  977. }
  978. private void UpdateButtonStates()
  979. {
  980. var hasInterfaces = _interfaces.Count > 0;
  981. InterfacesItemsControl.IsEnabled = !_isBusy && hasInterfaces;
  982. ReloadInterfaceConfigButton.IsEnabled = !_isBusy && hasInterfaces;
  983. ValidateConfigButton.IsEnabled = !_isBusy && hasInterfaces;
  984. ApplyConfigButton.IsEnabled = !_isBusy && _configValidated && hasInterfaces;
  985. RebootButton.IsEnabled = !_isBusy;
  986. ShutdownButton.IsEnabled = !_isBusy;
  987. }
  988. private void SetBusyState(bool isBusy, string? message = null)
  989. {
  990. _isBusy = isBusy;
  991. BusyOverlay.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
  992. BusyMessageTextBlock.Text = string.IsNullOrWhiteSpace(message) ? "正在处理,请稍候..." : message;
  993. UpdateButtonStates();
  994. }
  995. private sealed class InterfaceEditor : INotifyPropertyChanged
  996. {
  997. private bool _dhcp4;
  998. private bool _defaultGatewayEnabled;
  999. private bool _customRoutesEnabled;
  1000. private string _defaultGateway = string.Empty;
  1001. public InterfaceEditor(RemoteInterfaceInfo info)
  1002. {
  1003. SystemName = info.SystemName;
  1004. StatusSummary = info.StatusSummary;
  1005. LinkUp = info.LinkUp;
  1006. }
  1007. public string SystemName { get; }
  1008. public string StatusSummary { get; }
  1009. public bool LinkUp { get; }
  1010. public ObservableCollection<EditableAddress> Addresses { get; } = [];
  1011. public ObservableCollection<EditableRoute> Routes { get; } = [];
  1012. public ObservableCollection<EditableDns> Dns { get; } = [];
  1013. public bool Dhcp4
  1014. {
  1015. get => _dhcp4;
  1016. set => SetField(ref _dhcp4, value);
  1017. }
  1018. public bool DefaultGatewayEnabled
  1019. {
  1020. get => _defaultGatewayEnabled;
  1021. set => SetField(ref _defaultGatewayEnabled, value);
  1022. }
  1023. public bool CustomRoutesEnabled
  1024. {
  1025. get => _customRoutesEnabled;
  1026. set => SetField(ref _customRoutesEnabled, value);
  1027. }
  1028. public string DefaultGateway
  1029. {
  1030. get => _defaultGateway;
  1031. set => SetField(ref _defaultGateway, value);
  1032. }
  1033. public event PropertyChangedEventHandler? PropertyChanged;
  1034. private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
  1035. {
  1036. if (EqualityComparer<T>.Default.Equals(field, value))
  1037. {
  1038. return;
  1039. }
  1040. field = value;
  1041. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  1042. }
  1043. }
  1044. private sealed class EditableAddress : INotifyPropertyChanged
  1045. {
  1046. private string _ip = string.Empty;
  1047. private string _mask = string.Empty;
  1048. public EditableAddress(InterfaceEditor owner)
  1049. {
  1050. Owner = owner;
  1051. }
  1052. public InterfaceEditor Owner { get; }
  1053. public string IP
  1054. {
  1055. get => _ip;
  1056. set => SetField(ref _ip, value);
  1057. }
  1058. public string Mask
  1059. {
  1060. get => _mask;
  1061. set => SetField(ref _mask, value);
  1062. }
  1063. public event PropertyChangedEventHandler? PropertyChanged;
  1064. private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
  1065. {
  1066. if (EqualityComparer<T>.Default.Equals(field, value))
  1067. {
  1068. return;
  1069. }
  1070. field = value;
  1071. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  1072. }
  1073. }
  1074. private sealed class EditableRoute
  1075. {
  1076. public EditableRoute(InterfaceEditor owner)
  1077. {
  1078. Owner = owner;
  1079. }
  1080. public InterfaceEditor Owner { get; }
  1081. public string To { get; set; } = string.Empty;
  1082. public string Mask { get; set; } = string.Empty;
  1083. public string Via { get; set; } = string.Empty;
  1084. }
  1085. private sealed class EditableDns
  1086. {
  1087. public EditableDns(InterfaceEditor owner)
  1088. {
  1089. Owner = owner;
  1090. }
  1091. public InterfaceEditor Owner { get; }
  1092. public string Address { get; set; } = string.Empty;
  1093. }
  1094. }