DeviceDetailsWindow.xaml.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. using System.Globalization;
  2. using System.Windows;
  3. using System.Windows.Controls;
  4. using System.Windows.Media;
  5. using System.Windows.Media.Animation;
  6. using NetworkTool.Client.Models;
  7. using NetworkTool.Client.Services;
  8. namespace NetworkTool.Client;
  9. public partial class DeviceDetailsWindow : Window
  10. {
  11. private const int ApplyConfirmationTimeoutSeconds = 20;
  12. private readonly ServerApiService _serverApiService = new();
  13. private readonly string _baseAddress;
  14. private readonly string _localIPv4;
  15. private readonly string _password;
  16. private bool _configValidated;
  17. private bool _isBusy;
  18. private bool _suppressConfigChangeHandling;
  19. private CancellationTokenSource? _statusMessageCts;
  20. public DeviceDetailsWindow(string baseAddress, string localIPv4, string password)
  21. {
  22. InitializeComponent();
  23. _baseAddress = baseAddress;
  24. _localIPv4 = localIPv4;
  25. _password = password;
  26. Loaded += DeviceDetailsWindow_OnLoaded;
  27. }
  28. private async void DeviceDetailsWindow_OnLoaded(object sender, RoutedEventArgs e)
  29. {
  30. try
  31. {
  32. await LoadRemoteDetailsAsync();
  33. UpdateButtonStates();
  34. }
  35. catch (Exception ex)
  36. {
  37. ShowStatusMessage($"读取设备信息失败:{ex.Message}");
  38. SetBusyState(false);
  39. }
  40. }
  41. private async Task LoadRemoteDetailsAsync()
  42. {
  43. ClearDetails();
  44. var device = await _serverApiService.GetDeviceInfoAsync(_baseAddress, _password, _localIPv4);
  45. if (device is not null)
  46. {
  47. RemoteDeviceIdTextBlock.Text = device.DeviceId;
  48. RemoteHostnameTextBlock.Text = device.Hostname;
  49. RemoteOsVersionTextBlock.Text = device.OSVersion;
  50. RemoteServerVersionTextBlock.Text = device.ServerVersion;
  51. }
  52. var interfaces = await _serverApiService.GetInterfacesAsync(_baseAddress, _password, _localIPv4);
  53. if (interfaces is null)
  54. {
  55. ShowStatusMessage("设备已连接,但暂时无法读取 Linux 接口列表。");
  56. return;
  57. }
  58. ShowStatusMessage($"当前管理接口:{interfaces.ManagementInterface};建议目标接口:{interfaces.SuggestedTargetInterface};{(interfaces.RequiresTargetSelection ? "需要手动选择目标接口。" : "已自动识别建议目标接口。")}");
  59. var suggested = interfaces.Interfaces.FirstOrDefault(item => item.SystemName == interfaces.SuggestedTargetInterface)
  60. ?? interfaces.Interfaces.FirstOrDefault(item => item.IsSuggestedTarget)
  61. ?? interfaces.Interfaces.FirstOrDefault(item => !item.IsManagementInterface);
  62. RemoteTargetInterfaceComboBox.ItemsSource = interfaces.Interfaces;
  63. if (suggested is not null)
  64. {
  65. RemoteTargetInterfaceComboBox.SelectedItem = suggested;
  66. await LoadRemoteInterfaceConfigAsync(suggested.SystemName);
  67. }
  68. }
  69. private void ClearDetails()
  70. {
  71. RemoteDeviceIdTextBlock.Text = "-";
  72. RemoteHostnameTextBlock.Text = "-";
  73. RemoteOsVersionTextBlock.Text = "-";
  74. RemoteServerVersionTextBlock.Text = "-";
  75. RemoteTargetInterfaceComboBox.ItemsSource = null;
  76. RemoteConfigInterfaceTextBlock.Text = "-";
  77. RemoteConfigIpTextBlock.Text = "-";
  78. RemoteConfigGatewayTextBlock.Text = "-";
  79. RemoteConfigDnsTextBlock.Text = "-";
  80. NewAddressesTextBox.Text = string.Empty;
  81. NewRoutesTextBox.Text = string.Empty;
  82. NewDnsTextBox.Text = string.Empty;
  83. _configValidated = false;
  84. }
  85. private async void RemoteTargetInterfaceComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  86. {
  87. try
  88. {
  89. if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
  90. {
  91. UpdateButtonStates();
  92. return;
  93. }
  94. await LoadRemoteInterfaceConfigAsync(selected.SystemName, useBusyState: true);
  95. }
  96. catch (Exception ex)
  97. {
  98. ShowStatusMessage($"读取目标接口配置失败:{ex.Message}");
  99. SetBusyState(false);
  100. }
  101. }
  102. private async Task LoadRemoteInterfaceConfigAsync(string interfaceName, bool useBusyState = false)
  103. {
  104. if (useBusyState)
  105. {
  106. SetBusyState(true, "正在读取 Linux 端 IP 配置...");
  107. }
  108. try
  109. {
  110. var result = await _serverApiService.GetInterfaceConfigAsync(_baseAddress, _password, _localIPv4, interfaceName);
  111. if (!result.Success || result.Data is null)
  112. {
  113. RemoteConfigInterfaceTextBlock.Text = interfaceName;
  114. RemoteConfigIpTextBlock.Text = "读取失败";
  115. RemoteConfigGatewayTextBlock.Text = "读取失败";
  116. RemoteConfigDnsTextBlock.Text = "读取失败";
  117. ShowStatusMessage($"读取目标接口 {interfaceName} 配置失败:{result.Message}");
  118. return;
  119. }
  120. var config = result.Data;
  121. RemoteConfigInterfaceTextBlock.Text = config.Interface;
  122. RemoteConfigIpTextBlock.Text = FormatCurrentIp(config);
  123. RemoteConfigGatewayTextBlock.Text = FormatRoutes(config.EffectiveRoutes);
  124. RemoteConfigDnsTextBlock.Text = config.DnsSummary;
  125. _suppressConfigChangeHandling = true;
  126. Dhcp4CheckBox.IsChecked = false;
  127. NewAddressesTextBox.Text = string.Join(Environment.NewLine, config.EffectiveAddresses.Select(item => $"{item.IP}/{item.Prefix}"));
  128. NewRoutesTextBox.Text = string.Join(Environment.NewLine, config.EffectiveRoutes.Select(item => $"{item.To} via {item.Via}"));
  129. NewDnsTextBox.Text = config.Dns is null ? string.Empty : string.Join(Environment.NewLine, config.Dns);
  130. _suppressConfigChangeHandling = false;
  131. _configValidated = false;
  132. ShowStatusMessage("已读取Linux端IP配置。");
  133. UpdateButtonStates();
  134. }
  135. finally
  136. {
  137. if (useBusyState)
  138. {
  139. SetBusyState(false);
  140. }
  141. }
  142. }
  143. private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
  144. {
  145. if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
  146. {
  147. await LoadRemoteInterfaceConfigAsync(selected.SystemName);
  148. }
  149. }
  150. private async void ValidateConfigButton_OnClick(object sender, RoutedEventArgs e)
  151. {
  152. if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
  153. {
  154. return;
  155. }
  156. var request = BuildConfigRequest(selected.SystemName);
  157. if (request is null)
  158. {
  159. return;
  160. }
  161. SetBusyState(true, "正在校验配置,请稍候...");
  162. try
  163. {
  164. var result = await _serverApiService.ValidateInterfaceConfigAsync(_baseAddress, _password, _localIPv4, request);
  165. _configValidated = result.Success && result.Data?.Valid == true;
  166. if (result.Data is not null)
  167. {
  168. var warnings = result.Data.Warnings.Count > 0 ? $" 警告:{string.Join(";", result.Data.Warnings)}" : string.Empty;
  169. var errors = result.Data.Errors.Count > 0 ? $" 错误:{string.Join(";", result.Data.Errors)}" : string.Empty;
  170. ShowStatusMessage(_configValidated ? $"校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}");
  171. }
  172. else
  173. {
  174. ShowStatusMessage($"校验失败:{result.Message}");
  175. }
  176. UpdateButtonStates();
  177. }
  178. finally
  179. {
  180. SetBusyState(false);
  181. }
  182. }
  183. private async void ApplyConfigButton_OnClick(object sender, RoutedEventArgs e)
  184. {
  185. if (RemoteTargetInterfaceComboBox.SelectedItem is not RemoteInterfaceInfo selected)
  186. {
  187. return;
  188. }
  189. var request = BuildConfigRequest(selected.SystemName);
  190. if (request is null)
  191. {
  192. return;
  193. }
  194. var confirmMessage = $"将要把以下配置应用到接口 {selected.SystemName}:\n\n" +
  195. $"模式:{(request.Dhcp4 ? "DHCP 自动获取" : "静态 IPv4")}\n" +
  196. $"IP:{(request.Dhcp4 ? "自动获取" : FormatAddresses(request.Addresses))}\n" +
  197. $"路由:{(request.Dhcp4 ? "自动获取" : FormatRoutes(request.Routes))}\n" +
  198. $"DNS:{(request.Dns.Count == 0 ? "无" : string.Join(", ", request.Dns))}\n\n" +
  199. "请确认是否继续。";
  200. if (MessageBox.Show(this, confirmMessage, "确认应用配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
  201. {
  202. return;
  203. }
  204. SetBusyState(true, "正在提交并应用配置,请稍候...");
  205. try
  206. {
  207. var applyResult = await _serverApiService.ApplyInterfaceConfigAsync(_baseAddress, _password, _localIPv4, request);
  208. if (!applyResult.Success || applyResult.Data is null)
  209. {
  210. ShowStatusMessage($"提交配置任务失败:{applyResult.Message}");
  211. return;
  212. }
  213. ShowStatusMessage("配置任务已提交,正在应用并等待连通确认...");
  214. await PollTaskAsync(applyResult.Data.TaskId);
  215. }
  216. finally
  217. {
  218. SetBusyState(false);
  219. }
  220. }
  221. private async Task PollTaskAsync(string taskId)
  222. {
  223. var transientFailureCount = 0;
  224. var confirmationRequested = false;
  225. for (var i = 0; i < 20; i++)
  226. {
  227. await Task.Delay(1000);
  228. var result = await _serverApiService.GetTaskAsync(_baseAddress, _password, _localIPv4, taskId);
  229. if (!result.Success || result.Data is null)
  230. {
  231. if (result.StatusCode is null)
  232. {
  233. transientFailureCount++;
  234. ShowStatusMessage($"设备连接短暂中断,正在重试({transientFailureCount})。");
  235. continue;
  236. }
  237. ShowStatusMessage($"读取任务状态失败:{result.Message}");
  238. return;
  239. }
  240. transientFailureCount = 0;
  241. var task = result.Data;
  242. ShowStatusMessage(FormatTaskStatusMessage(task));
  243. if (task.Status == "running" && task.Step == "confirming" && !confirmationRequested)
  244. {
  245. confirmationRequested = true;
  246. var confirm = ShowApplyConfirmationDialog(ApplyConfirmationTimeoutSeconds);
  247. if (confirm)
  248. {
  249. var confirmResult = await _serverApiService.ConfirmApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
  250. ShowStatusMessage(confirmResult.Success ? "已发送保留配置确认。" : $"发送确认失败:{confirmResult.Message}");
  251. }
  252. else
  253. {
  254. var cancelResult = await _serverApiService.CancelApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
  255. ShowStatusMessage(cancelResult.Success ? "已取消保留配置,正在回滚。" : $"发送取消失败:{cancelResult.Message}");
  256. }
  257. }
  258. if (task.Status is "success" or "failed" or "rolled_back")
  259. {
  260. ShowTaskCompletionDialog(task);
  261. if (RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo selected)
  262. {
  263. await LoadRemoteInterfaceConfigAsync(selected.SystemName);
  264. }
  265. return;
  266. }
  267. }
  268. ShowStatusMessage($"任务 {taskId} 轮询超时,请稍后手动刷新。");
  269. }
  270. private bool ShowApplyConfirmationDialog(int timeoutSeconds)
  271. {
  272. var remaining = timeoutSeconds;
  273. var result = false;
  274. var messageTextBlock = new TextBlock
  275. {
  276. Width = 420,
  277. TextWrapping = TextWrapping.Wrap,
  278. FontSize = 13,
  279. Foreground = Brushes.Black,
  280. };
  281. var confirmButton = new Button
  282. {
  283. MinWidth = 88,
  284. MinHeight = 32,
  285. Margin = new Thickness(0, 0, 10, 0),
  286. Content = "确认保留",
  287. IsDefault = true,
  288. };
  289. var cancelButton = new Button
  290. {
  291. MinWidth = 88,
  292. MinHeight = 32,
  293. Content = "取消回滚",
  294. IsCancel = true,
  295. };
  296. var dialog = new Window
  297. {
  298. Title = "确认保留网络配置",
  299. Owner = this,
  300. WindowStartupLocation = WindowStartupLocation.CenterOwner,
  301. ResizeMode = ResizeMode.NoResize,
  302. SizeToContent = SizeToContent.WidthAndHeight,
  303. Content = new StackPanel
  304. {
  305. Margin = new Thickness(18),
  306. Children =
  307. {
  308. messageTextBlock,
  309. new StackPanel
  310. {
  311. Margin = new Thickness(0, 18, 0, 0),
  312. HorizontalAlignment = HorizontalAlignment.Right,
  313. Orientation = Orientation.Horizontal,
  314. Children = { confirmButton, cancelButton },
  315. },
  316. },
  317. },
  318. };
  319. void UpdateMessage()
  320. {
  321. messageTextBlock.Text = $"当前客户端仍可连接到设备。是否确认保留这次网络配置?\n\n剩余 {remaining} 秒;超时或取消时,Linux 端会自动回滚。";
  322. }
  323. var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
  324. timer.Tick += (_, _) =>
  325. {
  326. remaining--;
  327. if (remaining <= 0)
  328. {
  329. timer.Stop();
  330. dialog.DialogResult = false;
  331. dialog.Close();
  332. return;
  333. }
  334. UpdateMessage();
  335. };
  336. confirmButton.Click += (_, _) =>
  337. {
  338. result = true;
  339. dialog.DialogResult = true;
  340. dialog.Close();
  341. };
  342. cancelButton.Click += (_, _) =>
  343. {
  344. dialog.DialogResult = false;
  345. dialog.Close();
  346. };
  347. dialog.Closed += (_, _) => timer.Stop();
  348. UpdateMessage();
  349. timer.Start();
  350. dialog.ShowDialog();
  351. return result;
  352. }
  353. private async void RebootButton_OnClick(object sender, RoutedEventArgs e)
  354. {
  355. await ExecuteSystemActionAsync(
  356. "重启设备",
  357. "设备将立即重启,当前窗口和连接可能马上中断。是否继续?",
  358. () => _serverApiService.RebootAsync(_baseAddress, _password, _localIPv4));
  359. }
  360. private async void ShutdownButton_OnClick(object sender, RoutedEventArgs e)
  361. {
  362. await ExecuteSystemActionAsync(
  363. "关闭设备",
  364. "设备将立即关机,当前窗口和连接可能马上中断。是否继续?",
  365. () => _serverApiService.ShutdownAsync(_baseAddress, _password, _localIPv4));
  366. }
  367. private async Task ExecuteSystemActionAsync(string title, string confirmMessage, Func<Task<ApiCallResult<RemoteSystemActionResponse>>> action)
  368. {
  369. if (MessageBox.Show(this, confirmMessage, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning) != MessageBoxResult.OK)
  370. {
  371. return;
  372. }
  373. var result = await action();
  374. if (!result.Success || result.Data is null)
  375. {
  376. ShowStatusMessage($"{title}失败:{result.Message}");
  377. return;
  378. }
  379. ShowStatusMessage($"{title}任务已提交:{result.Data.TaskId}。命令已发出,设备可能立即断开。");
  380. }
  381. private RemoteInterfaceConfig? BuildConfigRequest(string interfaceName)
  382. {
  383. var dhcp4 = Dhcp4CheckBox.IsChecked == true;
  384. var addresses = Array.Empty<RemoteInterfaceAddressConfig>();
  385. var routes = Array.Empty<RemoteInterfaceRouteConfig>();
  386. if (!dhcp4 && string.IsNullOrWhiteSpace(NewAddressesTextBox.Text))
  387. {
  388. ShowStatusMessage("IP 地址不能为空,至少需要填写一行地址。");
  389. return null;
  390. }
  391. if (!dhcp4 && !TryParseAddresses(NewAddressesTextBox.Text, out addresses, out var addressError))
  392. {
  393. ShowStatusMessage(addressError);
  394. return null;
  395. }
  396. if (!dhcp4 && !TryParseRoutes(NewRoutesTextBox.Text, out routes, out var routeError))
  397. {
  398. ShowStatusMessage(routeError);
  399. return null;
  400. }
  401. var dns = ParseListText(NewDnsTextBox.Text);
  402. return new RemoteInterfaceConfig
  403. {
  404. Interface = interfaceName,
  405. Dhcp4 = dhcp4,
  406. Addresses = dhcp4 ? Array.Empty<RemoteInterfaceAddressConfig>() : addresses,
  407. Routes = dhcp4 ? Array.Empty<RemoteInterfaceRouteConfig>() : routes,
  408. Dns = dhcp4 ? Array.Empty<string>() : dns,
  409. };
  410. }
  411. private static string FormatCurrentIp(RemoteInterfaceConfig config)
  412. {
  413. if (config.EffectiveAddresses.Count == 0)
  414. {
  415. return config.Dhcp4 ? "DHCP 自动获取,暂无 IPv4" : "无";
  416. }
  417. var text = FormatAddresses(config.EffectiveAddresses);
  418. return config.Dhcp4 ? $"{text} (DHCP)" : text;
  419. }
  420. private static string FormatAddresses(IReadOnlyList<RemoteInterfaceAddressConfig> addresses)
  421. {
  422. return addresses.Count == 0 ? "无" : string.Join(Environment.NewLine, addresses.Select(item => $"{item.IP}/{item.Prefix}"));
  423. }
  424. private static string FormatRoutes(IReadOnlyList<RemoteInterfaceRouteConfig> routes)
  425. {
  426. return routes.Count == 0 ? "无" : string.Join(Environment.NewLine, routes.Select(item => $"{item.To} via {item.Via}"));
  427. }
  428. private static bool TryParseAddresses(string text, out RemoteInterfaceAddressConfig[] addresses, out string error)
  429. {
  430. var result = new List<RemoteInterfaceAddressConfig>();
  431. foreach (var line in ParseListText(text))
  432. {
  433. var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  434. if (parts.Length == 1 && line.Contains('/'))
  435. {
  436. var cidrParts = line.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  437. if (cidrParts.Length != 2 || !int.TryParse(cidrParts[1], out var prefix) || prefix < 0 || prefix > 32)
  438. {
  439. addresses = [];
  440. error = $"地址格式不正确:{line}";
  441. return false;
  442. }
  443. result.Add(new RemoteInterfaceAddressConfig { IP = cidrParts[0], Prefix = prefix });
  444. continue;
  445. }
  446. if (parts.Length != 2)
  447. {
  448. addresses = [];
  449. error = $"地址格式不正确:{line}";
  450. return false;
  451. }
  452. if (!TryMaskOrPrefixToPrefix(parts[1], out var parsedPrefix))
  453. {
  454. addresses = [];
  455. error = $"子网掩码或前缀格式不正确:{line}";
  456. return false;
  457. }
  458. result.Add(new RemoteInterfaceAddressConfig { IP = parts[0], Prefix = parsedPrefix });
  459. }
  460. addresses = result.ToArray();
  461. error = string.Empty;
  462. return addresses.Length > 0;
  463. }
  464. private static bool TryParseRoutes(string text, out RemoteInterfaceRouteConfig[] routes, out string error)
  465. {
  466. var result = new List<RemoteInterfaceRouteConfig>();
  467. foreach (var line in ParseListText(text))
  468. {
  469. var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  470. if (parts.Length == 1)
  471. {
  472. result.Add(new RemoteInterfaceRouteConfig { To = "default", Via = parts[0] });
  473. continue;
  474. }
  475. if (parts.Length == 3 && parts[1].Equals("via", StringComparison.OrdinalIgnoreCase))
  476. {
  477. result.Add(new RemoteInterfaceRouteConfig { To = parts[0], Via = parts[2] });
  478. continue;
  479. }
  480. routes = [];
  481. error = $"路由格式不正确:{line}";
  482. return false;
  483. }
  484. routes = result.ToArray();
  485. error = string.Empty;
  486. return true;
  487. }
  488. private static string[] ParseListText(string text)
  489. {
  490. return text.Split(['\r', '\n', ',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  491. }
  492. private static bool TryMaskOrPrefixToPrefix(string text, out int prefix)
  493. {
  494. if (int.TryParse(text, out prefix) && prefix >= 0 && prefix <= 32)
  495. {
  496. return true;
  497. }
  498. return TryMaskToPrefix(text, out prefix);
  499. }
  500. private static string PrefixToMask(int prefix)
  501. {
  502. if (prefix < 0 || prefix > 32)
  503. {
  504. return string.Empty;
  505. }
  506. var mask = prefix == 0 ? 0u : uint.MaxValue << (32 - prefix);
  507. return string.Join('.', new[] { (mask >> 24) & 255, (mask >> 16) & 255, (mask >> 8) & 255, mask & 255 });
  508. }
  509. private static bool TryMaskToPrefix(string maskText, out int prefix)
  510. {
  511. prefix = 0;
  512. if (string.IsNullOrWhiteSpace(maskText))
  513. {
  514. return false;
  515. }
  516. var parts = maskText.Trim().Split('.');
  517. if (parts.Length != 4)
  518. {
  519. return false;
  520. }
  521. uint mask = 0;
  522. foreach (var part in parts)
  523. {
  524. if (!byte.TryParse(part, out var octet))
  525. {
  526. return false;
  527. }
  528. mask = (mask << 8) | octet;
  529. }
  530. var seenZero = false;
  531. for (var i = 31; i >= 0; i--)
  532. {
  533. var bit = (mask & (1u << i)) != 0;
  534. if (bit && seenZero)
  535. {
  536. return false;
  537. }
  538. if (bit)
  539. {
  540. prefix++;
  541. }
  542. else
  543. {
  544. seenZero = true;
  545. }
  546. }
  547. return true;
  548. }
  549. private void ConfigInputChanged_OnChanged(object sender, TextChangedEventArgs e)
  550. {
  551. if (_suppressConfigChangeHandling)
  552. {
  553. return;
  554. }
  555. _configValidated = false;
  556. ShowStatusMessage("配置内容已变更,请重新点击“2. 校验配置”。");
  557. UpdateButtonStates();
  558. }
  559. private void ConfigModeChanged_OnChanged(object sender, RoutedEventArgs e)
  560. {
  561. if (_suppressConfigChangeHandling)
  562. {
  563. UpdateButtonStates();
  564. return;
  565. }
  566. _configValidated = false;
  567. ShowStatusMessage("配置模式已变更,请重新点击“2. 校验配置”。");
  568. UpdateButtonStates();
  569. }
  570. private void ShowStatusMessage(string message)
  571. {
  572. ApplyStatusMessageStyle(message);
  573. StatusMessageTextBlock.Text = message;
  574. StatusMessageBorder.Opacity = 0;
  575. StatusMessageBorder.Visibility = Visibility.Visible;
  576. StatusMessageBorder.BeginAnimation(OpacityProperty, new DoubleAnimation(1, TimeSpan.FromMilliseconds(160)));
  577. _statusMessageCts?.Cancel();
  578. _statusMessageCts = new CancellationTokenSource();
  579. _ = HideStatusMessageAsync(_statusMessageCts.Token);
  580. }
  581. private async Task HideStatusMessageAsync(CancellationToken cancellationToken)
  582. {
  583. try
  584. {
  585. await Task.Delay(3000, cancellationToken);
  586. await Dispatcher.InvokeAsync(() =>
  587. {
  588. var animation = new DoubleAnimation(0, TimeSpan.FromMilliseconds(200));
  589. animation.Completed += (_, _) =>
  590. {
  591. if (!cancellationToken.IsCancellationRequested)
  592. {
  593. StatusMessageBorder.Visibility = Visibility.Collapsed;
  594. }
  595. };
  596. StatusMessageBorder.BeginAnimation(OpacityProperty, animation);
  597. });
  598. }
  599. catch (TaskCanceledException)
  600. {
  601. }
  602. }
  603. private void ApplyStatusMessageStyle(string message)
  604. {
  605. var (background, foreground) = GetStatusMessageBrushes(message);
  606. StatusMessageBorder.Background = background;
  607. StatusMessageTextBlock.Foreground = foreground;
  608. }
  609. private static (Brush Background, Brush Foreground) GetStatusMessageBrushes(string message)
  610. {
  611. if (ContainsAny(message, "失败", "错误", "拒绝", "超时", "不能为空", "不正确", "无法"))
  612. {
  613. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B91C1C")), Brushes.White);
  614. }
  615. if (ContainsAny(message, "未发现", "请", "重试", "警告", "需要"))
  616. {
  617. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#C2410C")), Brushes.White);
  618. }
  619. if (ContainsAny(message, "成功", "已切换", "已刷新", "已读取", "已加载", "已发现", "已提交", "已回填"))
  620. {
  621. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#047857")), Brushes.White);
  622. }
  623. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#111827")), Brushes.White);
  624. }
  625. private static bool ContainsAny(string message, params string[] markers)
  626. {
  627. return markers.Any(marker => message.Contains(marker, StringComparison.Ordinal));
  628. }
  629. private static string FormatTaskStatusMessage(RemoteTaskResult task)
  630. {
  631. return task.Status switch
  632. {
  633. "success" => string.IsNullOrWhiteSpace(task.Detail) ? "配置已成功应用。" : task.Detail,
  634. "failed" => string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败。" : task.Detail,
  635. "rolled_back" => string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败,已自动回滚。" : task.Detail,
  636. _ => task.Step switch
  637. {
  638. "validating" => "正在校验配置...",
  639. "writing_netplan" => "正在写入 Linux 网络配置...",
  640. "applying" => "正在应用 Linux 网络配置...",
  641. "confirming" => string.IsNullOrWhiteSpace(task.Detail) ? "等待确认保留配置..." : task.Detail,
  642. "rolling_back" => "配置应用失败,正在自动回滚...",
  643. _ => string.IsNullOrWhiteSpace(task.Detail) ? "正在处理,请稍候..." : task.Detail,
  644. }
  645. };
  646. }
  647. private void ShowTaskCompletionDialog(RemoteTaskResult task)
  648. {
  649. var message = FormatTaskStatusMessage(task);
  650. var title = task.Status == "success" ? "应用配置成功" : "应用配置失败";
  651. var image = task.Status == "success" ? MessageBoxImage.Information : MessageBoxImage.Warning;
  652. MessageBox.Show(this, message, title, MessageBoxButton.OK, image);
  653. }
  654. private void UpdateButtonStates()
  655. {
  656. var hasSelectedInterface = RemoteTargetInterfaceComboBox.SelectedItem is RemoteInterfaceInfo;
  657. var canEdit = !_isBusy && hasSelectedInterface;
  658. RemoteTargetInterfaceComboBox.IsEnabled = !_isBusy && RemoteTargetInterfaceComboBox.Items.Count > 0;
  659. ReloadInterfaceConfigButton.IsEnabled = canEdit;
  660. ValidateConfigButton.IsEnabled = canEdit;
  661. ApplyConfigButton.IsEnabled = !_isBusy && _configValidated && hasSelectedInterface;
  662. Dhcp4CheckBox.IsEnabled = canEdit;
  663. NewAddressesTextBox.IsEnabled = canEdit && Dhcp4CheckBox.IsChecked != true;
  664. NewRoutesTextBox.IsEnabled = canEdit && Dhcp4CheckBox.IsChecked != true;
  665. NewDnsTextBox.IsEnabled = canEdit && Dhcp4CheckBox.IsChecked != true;
  666. RebootButton.IsEnabled = !_isBusy;
  667. ShutdownButton.IsEnabled = !_isBusy;
  668. }
  669. private void SetBusyState(bool isBusy, string? message = null)
  670. {
  671. _isBusy = isBusy;
  672. BusyOverlay.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
  673. BusyMessageTextBlock.Text = string.IsNullOrWhiteSpace(message) ? "正在处理,请稍候..." : message;
  674. UpdateButtonStates();
  675. }
  676. }