DeviceDetailsWindow.xaml.cs 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224
  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. LinkUp = info.LinkUp;
  1005. }
  1006. public string SystemName { get; }
  1007. public string StatusSummary { get; }
  1008. public bool LinkUp { get; }
  1009. public ObservableCollection<EditableAddress> Addresses { get; } = [];
  1010. public ObservableCollection<EditableRoute> Routes { get; } = [];
  1011. public ObservableCollection<EditableDns> Dns { get; } = [];
  1012. public bool Dhcp4
  1013. {
  1014. get => _dhcp4;
  1015. set => SetField(ref _dhcp4, value);
  1016. }
  1017. public bool DefaultGatewayEnabled
  1018. {
  1019. get => _defaultGatewayEnabled;
  1020. set => SetField(ref _defaultGatewayEnabled, value);
  1021. }
  1022. public bool CustomRoutesEnabled
  1023. {
  1024. get => _customRoutesEnabled;
  1025. set => SetField(ref _customRoutesEnabled, value);
  1026. }
  1027. public string DefaultGateway
  1028. {
  1029. get => _defaultGateway;
  1030. set => SetField(ref _defaultGateway, value);
  1031. }
  1032. public event PropertyChangedEventHandler? PropertyChanged;
  1033. private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
  1034. {
  1035. if (EqualityComparer<T>.Default.Equals(field, value))
  1036. {
  1037. return;
  1038. }
  1039. field = value;
  1040. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  1041. }
  1042. }
  1043. private sealed class EditableAddress : INotifyPropertyChanged
  1044. {
  1045. private string _ip = string.Empty;
  1046. private string _mask = string.Empty;
  1047. public EditableAddress(InterfaceEditor owner)
  1048. {
  1049. Owner = owner;
  1050. }
  1051. public InterfaceEditor Owner { get; }
  1052. public string IP
  1053. {
  1054. get => _ip;
  1055. set => SetField(ref _ip, value);
  1056. }
  1057. public string Mask
  1058. {
  1059. get => _mask;
  1060. set => SetField(ref _mask, value);
  1061. }
  1062. public event PropertyChangedEventHandler? PropertyChanged;
  1063. private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
  1064. {
  1065. if (EqualityComparer<T>.Default.Equals(field, value))
  1066. {
  1067. return;
  1068. }
  1069. field = value;
  1070. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  1071. }
  1072. }
  1073. private sealed class EditableRoute
  1074. {
  1075. public EditableRoute(InterfaceEditor owner)
  1076. {
  1077. Owner = owner;
  1078. }
  1079. public InterfaceEditor Owner { get; }
  1080. public string To { get; set; } = string.Empty;
  1081. public string Mask { get; set; } = string.Empty;
  1082. public string Via { get; set; } = string.Empty;
  1083. }
  1084. private sealed class EditableDns
  1085. {
  1086. public EditableDns(InterfaceEditor owner)
  1087. {
  1088. Owner = owner;
  1089. }
  1090. public InterfaceEditor Owner { get; }
  1091. public string Address { get; set; } = string.Empty;
  1092. }
  1093. }