DeviceDetailsWindow.xaml.cs 40 KB

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