DeviceDetailsWindow.xaml.cs 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222
  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. var cancelButton = new Button
  316. {
  317. MinWidth = 88,
  318. MinHeight = 32,
  319. Content = "取消回滚",
  320. IsCancel = true,
  321. };
  322. var dialog = new Window
  323. {
  324. Title = "确认保留网络配置",
  325. Owner = this,
  326. WindowStartupLocation = WindowStartupLocation.CenterOwner,
  327. ResizeMode = ResizeMode.NoResize,
  328. SizeToContent = SizeToContent.WidthAndHeight,
  329. Content = new StackPanel
  330. {
  331. Margin = new Thickness(18),
  332. Children =
  333. {
  334. messageTextBlock,
  335. new StackPanel
  336. {
  337. Margin = new Thickness(0, 18, 0, 0),
  338. HorizontalAlignment = HorizontalAlignment.Right,
  339. Orientation = Orientation.Horizontal,
  340. Children = { confirmButton, cancelButton },
  341. },
  342. },
  343. },
  344. };
  345. void UpdateMessage()
  346. {
  347. messageTextBlock.Text = $"当前客户端仍可连接到设备。是否确认保留这次网络配置?\n\n剩余 {remaining} 秒;超时或取消时,Linux 端会自动回滚。";
  348. }
  349. var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
  350. timer.Tick += (_, _) =>
  351. {
  352. remaining--;
  353. if (remaining <= 0)
  354. {
  355. timer.Stop();
  356. dialog.DialogResult = false;
  357. dialog.Close();
  358. return;
  359. }
  360. UpdateMessage();
  361. };
  362. confirmButton.Click += (_, _) =>
  363. {
  364. result = true;
  365. dialog.DialogResult = true;
  366. dialog.Close();
  367. };
  368. cancelButton.Click += (_, _) =>
  369. {
  370. dialog.DialogResult = false;
  371. dialog.Close();
  372. };
  373. dialog.Closed += (_, _) => timer.Stop();
  374. UpdateMessage();
  375. timer.Start();
  376. dialog.ShowDialog();
  377. return result;
  378. }
  379. private async void RebootButton_OnClick(object sender, RoutedEventArgs e)
  380. {
  381. await ExecuteSystemActionAsync(
  382. "重启设备",
  383. "设备将立即重启,当前窗口和连接可能马上中断。是否继续?",
  384. () => _serverApiService.RebootAsync(_baseAddress, _password, _localIPv4));
  385. }
  386. private async void ShutdownButton_OnClick(object sender, RoutedEventArgs e)
  387. {
  388. await ExecuteSystemActionAsync(
  389. "关闭设备",
  390. "设备将立即关机,当前窗口和连接可能马上中断。是否继续?",
  391. () => _serverApiService.ShutdownAsync(_baseAddress, _password, _localIPv4));
  392. }
  393. private async Task ExecuteSystemActionAsync(string title, string confirmMessage, Func<Task<ApiCallResult<RemoteSystemActionResponse>>> action)
  394. {
  395. if (MessageBox.Show(this, confirmMessage, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning) != MessageBoxResult.OK)
  396. {
  397. return;
  398. }
  399. var result = await action();
  400. if (!result.Success || result.Data is null)
  401. {
  402. ShowStatusMessage($"{title}失败:{result.Message}");
  403. return;
  404. }
  405. ShowStatusMessage($"{title}任务已提交:{result.Data.TaskId}。命令已发出,设备可能立即断开。");
  406. }
  407. private RemoteInterfaceConfig[]? BuildConfigRequests()
  408. {
  409. CommitConfigEdits();
  410. var result = new List<RemoteInterfaceConfig>();
  411. foreach (var editor in _interfaces)
  412. {
  413. var request = BuildConfigRequest(editor);
  414. if (request is null)
  415. {
  416. return null;
  417. }
  418. result.Add(request);
  419. }
  420. if (result.Count == 0)
  421. {
  422. ShowStatusMessage("接口配置不能为空。");
  423. return null;
  424. }
  425. return result.ToArray();
  426. }
  427. private RemoteInterfaceConfig? BuildConfigRequest(InterfaceEditor editor)
  428. {
  429. var dhcp4 = editor.Dhcp4;
  430. var addresses = Array.Empty<RemoteInterfaceAddressConfig>();
  431. var routes = Array.Empty<RemoteInterfaceRouteConfig>();
  432. if (!dhcp4)
  433. {
  434. if (editor.Addresses.All(item => string.IsNullOrWhiteSpace(item.IP) && string.IsNullOrWhiteSpace(item.Mask)))
  435. {
  436. ShowStatusMessage($"{editor.SystemName}:IP 地址不能为空,至少需要填写一行地址。");
  437. return null;
  438. }
  439. if (!TryBuildAddresses(editor, out addresses, out var addressError))
  440. {
  441. ShowStatusMessage($"{editor.SystemName}:{addressError}");
  442. return null;
  443. }
  444. if (!TryBuildRoutes(editor, out routes, out var routeError))
  445. {
  446. ShowStatusMessage($"{editor.SystemName}:{routeError}");
  447. return null;
  448. }
  449. }
  450. var dns = editor.Dns.Select(item => item.Address.Trim()).Where(item => item != string.Empty).ToArray();
  451. return new RemoteInterfaceConfig
  452. {
  453. Interface = editor.SystemName,
  454. Dhcp4 = dhcp4,
  455. Addresses = dhcp4 ? Array.Empty<RemoteInterfaceAddressConfig>() : addresses,
  456. Routes = dhcp4 ? Array.Empty<RemoteInterfaceRouteConfig>() : routes,
  457. Dns = dns,
  458. };
  459. }
  460. private void CommitConfigEdits()
  461. {
  462. CommitDataGridEdits(InterfacesItemsControl);
  463. }
  464. private static void CommitDataGridEdits(DependencyObject root)
  465. {
  466. for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
  467. {
  468. var child = VisualTreeHelper.GetChild(root, i);
  469. if (child is DataGrid dataGrid)
  470. {
  471. dataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
  472. dataGrid.CommitEdit(DataGridEditingUnit.Row, true);
  473. }
  474. CommitDataGridEdits(child);
  475. }
  476. }
  477. private static string FormatCurrentIp(RemoteInterfaceConfig config)
  478. {
  479. if (config.EffectiveAddresses.Count == 0)
  480. {
  481. return config.Dhcp4 ? "DHCP 自动获取,暂无 IPv4" : "无";
  482. }
  483. var text = FormatAddresses(config.EffectiveAddresses);
  484. return config.Dhcp4 ? $"{text} (DHCP)" : text;
  485. }
  486. private static string FormatAddresses(IReadOnlyList<RemoteInterfaceAddressConfig> addresses)
  487. {
  488. return addresses.Count == 0 ? "无" : string.Join(Environment.NewLine, addresses.Select(item => $"{item.IP}/{item.Prefix}"));
  489. }
  490. private static string FormatRoutes(IReadOnlyList<RemoteInterfaceRouteConfig> routes)
  491. {
  492. return routes.Count == 0 ? "无" : string.Join(Environment.NewLine, routes.Select(item => $"{item.To} via {item.Via}"));
  493. }
  494. private static string FormatConfigSummary(IReadOnlyList<RemoteInterfaceConfig> configs)
  495. {
  496. return string.Join(Environment.NewLine + Environment.NewLine, configs.Select(item =>
  497. $"接口:{item.Interface}\n" +
  498. $"模式:{(item.Dhcp4 ? "DHCP 自动获取" : "静态 IPv4")}\n" +
  499. $"IP:{(item.Dhcp4 ? "自动获取" : FormatAddresses(item.Addresses))}\n" +
  500. $"路由:{(item.Dhcp4 ? "自动获取" : FormatRoutes(item.Routes))}\n" +
  501. $"DNS:{(item.Dns.Count == 0 ? "无" : string.Join(", ", item.Dns))}"));
  502. }
  503. private static EditableRoute CreateEditableRoute(InterfaceEditor owner, RemoteInterfaceRouteConfig route)
  504. {
  505. var to = route.To.Trim();
  506. var mask = string.Empty;
  507. if (to.Contains('/'))
  508. {
  509. var parts = to.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  510. if (parts.Length == 2 && int.TryParse(parts[1], out var prefix) && prefix >= 0 && prefix <= 32)
  511. {
  512. to = parts[0];
  513. mask = PrefixToMask(prefix);
  514. }
  515. }
  516. return new EditableRoute(owner) { To = to, Mask = mask, Via = route.Via };
  517. }
  518. private bool TryBuildAddresses(InterfaceEditor editor, out RemoteInterfaceAddressConfig[] addresses, out string error)
  519. {
  520. var result = new List<RemoteInterfaceAddressConfig>();
  521. foreach (var row in editor.Addresses)
  522. {
  523. var ip = row.IP.Trim();
  524. var maskText = row.Mask.Trim();
  525. if (ip == string.Empty && maskText == string.Empty)
  526. {
  527. continue;
  528. }
  529. if (ip.Contains('/'))
  530. {
  531. var parts = ip.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  532. if (parts.Length != 2 || !int.TryParse(parts[1], out var cidrPrefix) || cidrPrefix < 0 || cidrPrefix > 32)
  533. {
  534. addresses = [];
  535. error = $"地址格式不正确:{ip}";
  536. return false;
  537. }
  538. row.IP = parts[0];
  539. row.Mask = PrefixToMask(cidrPrefix);
  540. result.Add(new RemoteInterfaceAddressConfig { IP = parts[0], Prefix = cidrPrefix });
  541. continue;
  542. }
  543. if (ip == string.Empty || maskText == string.Empty)
  544. {
  545. addresses = [];
  546. error = "IP 地址和子网掩码都需要填写。";
  547. return false;
  548. }
  549. if (!TryMaskOrPrefixToPrefix(maskText, out var prefix))
  550. {
  551. addresses = [];
  552. error = $"子网掩码格式不正确:{ip} {maskText}";
  553. return false;
  554. }
  555. row.Mask = PrefixToMask(prefix);
  556. result.Add(new RemoteInterfaceAddressConfig { IP = ip, Prefix = prefix });
  557. }
  558. addresses = result.ToArray();
  559. error = string.Empty;
  560. return addresses.Length > 0;
  561. }
  562. private bool TryBuildRoutes(InterfaceEditor editor, out RemoteInterfaceRouteConfig[] routes, out string error)
  563. {
  564. var result = new List<RemoteInterfaceRouteConfig>();
  565. if (editor.DefaultGatewayEnabled)
  566. {
  567. var gateway = editor.DefaultGateway.Trim();
  568. if (gateway == string.Empty)
  569. {
  570. routes = [];
  571. error = "启用默认网关时,网关地址不能为空。";
  572. return false;
  573. }
  574. result.Add(new RemoteInterfaceRouteConfig { To = "default", Via = gateway });
  575. }
  576. if (editor.CustomRoutesEnabled)
  577. {
  578. foreach (var row in editor.Routes)
  579. {
  580. var to = row.To.Trim();
  581. var maskText = row.Mask.Trim();
  582. var via = row.Via.Trim();
  583. if (to == string.Empty && maskText == string.Empty && via == string.Empty)
  584. {
  585. continue;
  586. }
  587. if (to.Contains('/'))
  588. {
  589. var parts = to.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  590. if (parts.Length != 2 || !int.TryParse(parts[1], out var cidrPrefix) || cidrPrefix < 0 || cidrPrefix > 32)
  591. {
  592. routes = [];
  593. error = $"自定义路由目标网段格式不正确:{to}";
  594. return false;
  595. }
  596. to = parts[0];
  597. maskText = PrefixToMask(cidrPrefix);
  598. row.To = to;
  599. row.Mask = maskText;
  600. }
  601. if (to == string.Empty || maskText == string.Empty || via == string.Empty)
  602. {
  603. routes = [];
  604. error = "自定义路由的目标网段、子网掩码和网关地址都需要填写。";
  605. return false;
  606. }
  607. if (!TryMaskOrPrefixToPrefix(maskText, out var prefix))
  608. {
  609. routes = [];
  610. error = $"自定义路由子网掩码格式不正确:{to} {maskText}";
  611. return false;
  612. }
  613. row.Mask = PrefixToMask(prefix);
  614. result.Add(new RemoteInterfaceRouteConfig { To = $"{to}/{prefix}", Via = via });
  615. }
  616. }
  617. routes = result.ToArray();
  618. error = string.Empty;
  619. return true;
  620. }
  621. private static bool TryParseAddresses(string text, out RemoteInterfaceAddressConfig[] addresses, out string error)
  622. {
  623. var result = new List<RemoteInterfaceAddressConfig>();
  624. foreach (var line in ParseListText(text))
  625. {
  626. var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  627. if (parts.Length == 1 && line.Contains('/'))
  628. {
  629. var cidrParts = line.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  630. if (cidrParts.Length != 2 || !int.TryParse(cidrParts[1], out var prefix) || prefix < 0 || prefix > 32)
  631. {
  632. addresses = [];
  633. error = $"地址格式不正确:{line}";
  634. return false;
  635. }
  636. result.Add(new RemoteInterfaceAddressConfig { IP = cidrParts[0], Prefix = prefix });
  637. continue;
  638. }
  639. if (parts.Length != 2)
  640. {
  641. addresses = [];
  642. error = $"地址格式不正确:{line}";
  643. return false;
  644. }
  645. if (!TryMaskOrPrefixToPrefix(parts[1], out var parsedPrefix))
  646. {
  647. addresses = [];
  648. error = $"子网掩码或前缀格式不正确:{line}";
  649. return false;
  650. }
  651. result.Add(new RemoteInterfaceAddressConfig { IP = parts[0], Prefix = parsedPrefix });
  652. }
  653. addresses = result.ToArray();
  654. error = string.Empty;
  655. return addresses.Length > 0;
  656. }
  657. private static bool TryParseRoutes(string text, out RemoteInterfaceRouteConfig[] routes, out string error)
  658. {
  659. var result = new List<RemoteInterfaceRouteConfig>();
  660. foreach (var line in ParseListText(text))
  661. {
  662. var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  663. if (parts.Length == 1)
  664. {
  665. result.Add(new RemoteInterfaceRouteConfig { To = "default", Via = parts[0] });
  666. continue;
  667. }
  668. if (parts.Length == 3 && parts[1].Equals("via", StringComparison.OrdinalIgnoreCase))
  669. {
  670. result.Add(new RemoteInterfaceRouteConfig { To = parts[0], Via = parts[2] });
  671. continue;
  672. }
  673. routes = [];
  674. error = $"路由格式不正确:{line}";
  675. return false;
  676. }
  677. routes = result.ToArray();
  678. error = string.Empty;
  679. return true;
  680. }
  681. private static string[] ParseListText(string text)
  682. {
  683. return text.Split(['\r', '\n', ',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  684. }
  685. private static bool TryMaskOrPrefixToPrefix(string text, out int prefix)
  686. {
  687. if (int.TryParse(text, out prefix) && prefix >= 0 && prefix <= 32)
  688. {
  689. return true;
  690. }
  691. return TryMaskToPrefix(text, out prefix);
  692. }
  693. private static string PrefixToMask(int prefix)
  694. {
  695. if (prefix < 0 || prefix > 32)
  696. {
  697. return string.Empty;
  698. }
  699. var mask = prefix == 0 ? 0u : uint.MaxValue << (32 - prefix);
  700. return string.Join('.', new[] { (mask >> 24) & 255, (mask >> 16) & 255, (mask >> 8) & 255, mask & 255 });
  701. }
  702. private static bool TryMaskToPrefix(string maskText, out int prefix)
  703. {
  704. prefix = 0;
  705. if (string.IsNullOrWhiteSpace(maskText))
  706. {
  707. return false;
  708. }
  709. var parts = maskText.Trim().Split('.');
  710. if (parts.Length != 4)
  711. {
  712. return false;
  713. }
  714. uint mask = 0;
  715. foreach (var part in parts)
  716. {
  717. if (!byte.TryParse(part, out var octet))
  718. {
  719. return false;
  720. }
  721. mask = (mask << 8) | octet;
  722. }
  723. var seenZero = false;
  724. for (var i = 31; i >= 0; i--)
  725. {
  726. var bit = (mask & (1u << i)) != 0;
  727. if (bit && seenZero)
  728. {
  729. return false;
  730. }
  731. if (bit)
  732. {
  733. prefix++;
  734. }
  735. else
  736. {
  737. seenZero = true;
  738. }
  739. }
  740. return true;
  741. }
  742. private void ConfigInputChanged_OnChanged(object sender, TextChangedEventArgs e)
  743. {
  744. if (_suppressConfigChangeHandling)
  745. {
  746. return;
  747. }
  748. _configValidated = false;
  749. ShowStatusMessage("配置内容已变更,请重新点击“2. 校验配置”。");
  750. UpdateButtonStates();
  751. }
  752. private void ConfigModeChanged_OnChanged(object sender, RoutedEventArgs e)
  753. {
  754. if (_suppressConfigChangeHandling)
  755. {
  756. UpdateButtonStates();
  757. return;
  758. }
  759. _configValidated = false;
  760. _configDirty = true;
  761. ShowStatusMessage("配置模式已变更,请重新点击“2. 校验配置”。");
  762. UpdateButtonStates();
  763. }
  764. private void GatewayOrRouteModeChanged_OnChanged(object sender, RoutedEventArgs e)
  765. {
  766. if (_suppressConfigChangeHandling)
  767. {
  768. UpdateButtonStates();
  769. return;
  770. }
  771. _configValidated = false;
  772. _configDirty = true;
  773. ShowStatusMessage("配置内容已变更,请重新点击“2. 校验配置”。");
  774. UpdateButtonStates();
  775. }
  776. private void ConfigGrid_OnCellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
  777. {
  778. if (e.Row.Item is EditableAddress address)
  779. {
  780. NormalizeAddressRow(address);
  781. }
  782. else if (e.Row.Item is EditableRoute route)
  783. {
  784. NormalizeRouteRow(route);
  785. }
  786. MarkConfigChanged("配置内容已变更,请重新点击“2. 校验配置”。");
  787. }
  788. private void DataGrid_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
  789. {
  790. if (sender is not DataGrid dataGrid)
  791. {
  792. return;
  793. }
  794. e.Handled = true;
  795. var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
  796. {
  797. RoutedEvent = MouseWheelEvent,
  798. Source = dataGrid,
  799. };
  800. ContentScrollViewer.RaiseEvent(eventArg);
  801. }
  802. private void AddAddressButton_OnClick(object sender, RoutedEventArgs e)
  803. {
  804. if ((sender as FrameworkElement)?.DataContext is not InterfaceEditor editor)
  805. {
  806. return;
  807. }
  808. editor.Addresses.Add(new EditableAddress(editor) { Mask = "255.255.255.0" });
  809. MarkConfigChanged("已添加 IP 地址,请填写后重新校验配置。");
  810. }
  811. private void AddRouteButton_OnClick(object sender, RoutedEventArgs e)
  812. {
  813. if ((sender as FrameworkElement)?.DataContext is not InterfaceEditor editor)
  814. {
  815. return;
  816. }
  817. editor.Routes.Add(new EditableRoute(editor));
  818. MarkConfigChanged("已添加路由,请填写后重新校验配置。");
  819. }
  820. private void AddDnsButton_OnClick(object sender, RoutedEventArgs e)
  821. {
  822. if ((sender as FrameworkElement)?.DataContext is not InterfaceEditor editor)
  823. {
  824. return;
  825. }
  826. editor.Dns.Add(new EditableDns(editor));
  827. MarkConfigChanged("已添加 DNS,请填写后重新校验配置。");
  828. }
  829. private void DeleteAddressButton_OnClick(object sender, RoutedEventArgs e)
  830. {
  831. if ((sender as FrameworkElement)?.DataContext is not EditableAddress address)
  832. {
  833. return;
  834. }
  835. address.Owner.Addresses.Remove(address);
  836. MarkConfigChanged("已删除 IP 地址,请重新校验配置。");
  837. }
  838. private void DeleteRouteButton_OnClick(object sender, RoutedEventArgs e)
  839. {
  840. if ((sender as FrameworkElement)?.DataContext is EditableRoute route)
  841. {
  842. route.Owner.Routes.Remove(route);
  843. MarkConfigChanged("已删除路由,请重新校验配置。");
  844. }
  845. }
  846. private void DeleteDnsButton_OnClick(object sender, RoutedEventArgs e)
  847. {
  848. if ((sender as FrameworkElement)?.DataContext is EditableDns dns)
  849. {
  850. dns.Owner.Dns.Remove(dns);
  851. MarkConfigChanged("已删除 DNS,请重新校验配置。");
  852. }
  853. }
  854. private void MarkConfigChanged(string message)
  855. {
  856. if (_suppressConfigChangeHandling)
  857. {
  858. return;
  859. }
  860. _configValidated = false;
  861. _configDirty = true;
  862. ShowStatusMessage(message);
  863. UpdateButtonStates();
  864. }
  865. private static void NormalizeAddressRow(EditableAddress row)
  866. {
  867. var ip = row.IP.Trim();
  868. if (!ip.Contains('/'))
  869. {
  870. return;
  871. }
  872. var parts = ip.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  873. if (parts.Length == 2 && int.TryParse(parts[1], out var prefix) && prefix >= 0 && prefix <= 32)
  874. {
  875. row.IP = parts[0];
  876. row.Mask = PrefixToMask(prefix);
  877. }
  878. }
  879. private static void NormalizeRouteRow(EditableRoute row)
  880. {
  881. var to = row.To.Trim();
  882. if (!to.Contains('/'))
  883. {
  884. return;
  885. }
  886. var parts = to.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  887. if (parts.Length == 2 && int.TryParse(parts[1], out var prefix) && prefix >= 0 && prefix <= 32)
  888. {
  889. row.To = parts[0];
  890. row.Mask = PrefixToMask(prefix);
  891. }
  892. }
  893. private void ShowStatusMessage(string message)
  894. {
  895. ApplyStatusMessageStyle(message);
  896. StatusMessageTextBlock.Text = message;
  897. StatusMessageBorder.Opacity = 0;
  898. StatusMessageBorder.Visibility = Visibility.Visible;
  899. StatusMessageBorder.BeginAnimation(OpacityProperty, new DoubleAnimation(1, TimeSpan.FromMilliseconds(160)));
  900. _statusMessageCts?.Cancel();
  901. _statusMessageCts = new CancellationTokenSource();
  902. _ = HideStatusMessageAsync(_statusMessageCts.Token);
  903. }
  904. private async Task HideStatusMessageAsync(CancellationToken cancellationToken)
  905. {
  906. try
  907. {
  908. await Task.Delay(3000, cancellationToken);
  909. await Dispatcher.InvokeAsync(() =>
  910. {
  911. var animation = new DoubleAnimation(0, TimeSpan.FromMilliseconds(200));
  912. animation.Completed += (_, _) =>
  913. {
  914. if (!cancellationToken.IsCancellationRequested)
  915. {
  916. StatusMessageBorder.Visibility = Visibility.Collapsed;
  917. }
  918. };
  919. StatusMessageBorder.BeginAnimation(OpacityProperty, animation);
  920. });
  921. }
  922. catch (TaskCanceledException)
  923. {
  924. }
  925. }
  926. private void ApplyStatusMessageStyle(string message)
  927. {
  928. var (background, foreground) = GetStatusMessageBrushes(message);
  929. StatusMessageBorder.Background = background;
  930. StatusMessageTextBlock.Foreground = foreground;
  931. }
  932. private static (Brush Background, Brush Foreground) GetStatusMessageBrushes(string message)
  933. {
  934. if (ContainsAny(message, "失败", "错误", "拒绝", "超时", "不能为空", "不正确", "无法"))
  935. {
  936. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B91C1C")), Brushes.White);
  937. }
  938. if (ContainsAny(message, "未发现", "请", "重试", "警告", "需要"))
  939. {
  940. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#C2410C")), Brushes.White);
  941. }
  942. if (ContainsAny(message, "成功", "已切换", "已刷新", "已读取", "已加载", "已发现", "已提交", "已回填"))
  943. {
  944. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#047857")), Brushes.White);
  945. }
  946. return (new SolidColorBrush((Color)ColorConverter.ConvertFromString("#111827")), Brushes.White);
  947. }
  948. private static bool ContainsAny(string message, params string[] markers)
  949. {
  950. return markers.Any(marker => message.Contains(marker, StringComparison.Ordinal));
  951. }
  952. private static string FormatTaskStatusMessage(RemoteTaskResult task)
  953. {
  954. return task.Status switch
  955. {
  956. "success" => string.IsNullOrWhiteSpace(task.Detail) ? "配置已成功应用。" : task.Detail,
  957. "failed" => string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败。" : task.Detail,
  958. "rolled_back" => string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败,已自动回滚。" : task.Detail,
  959. _ => task.Step switch
  960. {
  961. "validating" => "正在校验配置...",
  962. "writing_netplan" => "正在写入 Linux 网络配置...",
  963. "applying" => "正在应用 Linux 网络配置...",
  964. "confirming" => string.IsNullOrWhiteSpace(task.Detail) ? "等待确认保留配置..." : task.Detail,
  965. "rolling_back" => "配置应用失败,正在自动回滚...",
  966. _ => string.IsNullOrWhiteSpace(task.Detail) ? "正在处理,请稍候..." : task.Detail,
  967. }
  968. };
  969. }
  970. private void ShowTaskCompletionDialog(RemoteTaskResult task)
  971. {
  972. var message = FormatTaskStatusMessage(task);
  973. var title = task.Status == "success" ? "应用配置成功" : "应用配置失败";
  974. var image = task.Status == "success" ? MessageBoxImage.Information : MessageBoxImage.Warning;
  975. MessageBox.Show(this, message, title, MessageBoxButton.OK, image);
  976. }
  977. private void UpdateButtonStates()
  978. {
  979. var hasInterfaces = _interfaces.Count > 0;
  980. InterfacesItemsControl.IsEnabled = !_isBusy && hasInterfaces;
  981. ReloadInterfaceConfigButton.IsEnabled = !_isBusy && hasInterfaces;
  982. ValidateConfigButton.IsEnabled = !_isBusy && hasInterfaces;
  983. ApplyConfigButton.IsEnabled = !_isBusy && _configValidated && hasInterfaces;
  984. RebootButton.IsEnabled = !_isBusy;
  985. ShutdownButton.IsEnabled = !_isBusy;
  986. }
  987. private void SetBusyState(bool isBusy, string? message = null)
  988. {
  989. _isBusy = isBusy;
  990. BusyOverlay.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
  991. BusyMessageTextBlock.Text = string.IsNullOrWhiteSpace(message) ? "正在处理,请稍候..." : message;
  992. UpdateButtonStates();
  993. }
  994. private sealed class InterfaceEditor : INotifyPropertyChanged
  995. {
  996. private bool _dhcp4;
  997. private bool _defaultGatewayEnabled;
  998. private bool _customRoutesEnabled;
  999. private string _defaultGateway = string.Empty;
  1000. public InterfaceEditor(RemoteInterfaceInfo info)
  1001. {
  1002. SystemName = info.SystemName;
  1003. StatusSummary = info.StatusSummary;
  1004. }
  1005. public string SystemName { get; }
  1006. public string StatusSummary { get; }
  1007. public ObservableCollection<EditableAddress> Addresses { get; } = [];
  1008. public ObservableCollection<EditableRoute> Routes { get; } = [];
  1009. public ObservableCollection<EditableDns> Dns { get; } = [];
  1010. public bool Dhcp4
  1011. {
  1012. get => _dhcp4;
  1013. set => SetField(ref _dhcp4, value);
  1014. }
  1015. public bool DefaultGatewayEnabled
  1016. {
  1017. get => _defaultGatewayEnabled;
  1018. set => SetField(ref _defaultGatewayEnabled, value);
  1019. }
  1020. public bool CustomRoutesEnabled
  1021. {
  1022. get => _customRoutesEnabled;
  1023. set => SetField(ref _customRoutesEnabled, value);
  1024. }
  1025. public string DefaultGateway
  1026. {
  1027. get => _defaultGateway;
  1028. set => SetField(ref _defaultGateway, value);
  1029. }
  1030. public event PropertyChangedEventHandler? PropertyChanged;
  1031. private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
  1032. {
  1033. if (EqualityComparer<T>.Default.Equals(field, value))
  1034. {
  1035. return;
  1036. }
  1037. field = value;
  1038. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  1039. }
  1040. }
  1041. private sealed class EditableAddress : INotifyPropertyChanged
  1042. {
  1043. private string _ip = string.Empty;
  1044. private string _mask = string.Empty;
  1045. public EditableAddress(InterfaceEditor owner)
  1046. {
  1047. Owner = owner;
  1048. }
  1049. public InterfaceEditor Owner { get; }
  1050. public string IP
  1051. {
  1052. get => _ip;
  1053. set => SetField(ref _ip, value);
  1054. }
  1055. public string Mask
  1056. {
  1057. get => _mask;
  1058. set => SetField(ref _mask, value);
  1059. }
  1060. public event PropertyChangedEventHandler? PropertyChanged;
  1061. private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
  1062. {
  1063. if (EqualityComparer<T>.Default.Equals(field, value))
  1064. {
  1065. return;
  1066. }
  1067. field = value;
  1068. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  1069. }
  1070. }
  1071. private sealed class EditableRoute
  1072. {
  1073. public EditableRoute(InterfaceEditor owner)
  1074. {
  1075. Owner = owner;
  1076. }
  1077. public InterfaceEditor Owner { get; }
  1078. public string To { get; set; } = string.Empty;
  1079. public string Mask { get; set; } = string.Empty;
  1080. public string Via { get; set; } = string.Empty;
  1081. }
  1082. private sealed class EditableDns
  1083. {
  1084. public EditableDns(InterfaceEditor owner)
  1085. {
  1086. Owner = owner;
  1087. }
  1088. public InterfaceEditor Owner { get; }
  1089. public string Address { get; set; } = string.Empty;
  1090. }
  1091. }