Skip to content

Commit 59626ce

Browse files
committed
Feature: Auto reload, refactoring & export hosts
1 parent 667756f commit 59626ce

14 files changed

+261
-45
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Xml.Linq;
7+
using NETworkManager.Models.HostsFileEditor;
8+
using Newtonsoft.Json;
9+
10+
namespace NETworkManager.Models.Export;
11+
12+
public static partial class ExportManager
13+
{
14+
/// <summary>
15+
/// Method to export objects from type <see cref="HostsFileEntry" /> to a file.
16+
/// </summary>
17+
/// <param name="filePath">Path to the export file.</param>
18+
/// <param name="fileType">Allowed <see cref="ExportFileType" /> are CSV, XML or JSON.</param>
19+
/// <param name="collection">Objects as <see cref="IReadOnlyList{HostsFileEntry}" /> to export.</param>
20+
public static void Export(string filePath, ExportFileType fileType, IReadOnlyList<HostsFileEntry> collection)
21+
{
22+
switch (fileType)
23+
{
24+
case ExportFileType.Csv:
25+
CreateCsv(collection, filePath);
26+
break;
27+
case ExportFileType.Xml:
28+
CreateXml(collection, filePath);
29+
break;
30+
case ExportFileType.Json:
31+
CreateJson(collection, filePath);
32+
break;
33+
case ExportFileType.Txt:
34+
default:
35+
throw new ArgumentOutOfRangeException(nameof(fileType), fileType, null);
36+
}
37+
}
38+
39+
/// <summary>
40+
/// Creates a CSV file from the given <see cref="HostsFileEntry" /> collection.
41+
/// </summary>
42+
/// <param name="collection">Objects as <see cref="IReadOnlyList{HostsFileEntry}" /> to export.</param>
43+
/// <param name="filePath">Path to the export file.</param>
44+
private static void CreateCsv(IEnumerable<HostsFileEntry> collection, string filePath)
45+
{
46+
var stringBuilder = new StringBuilder();
47+
48+
stringBuilder.AppendLine(
49+
$"{nameof(HostsFileEntry.IsEnabled)},{nameof(HostsFileEntry.IPAddress)},{nameof(HostsFileEntry.Hostname)},{nameof(HostsFileEntry.Comment)}");
50+
51+
foreach (var info in collection)
52+
stringBuilder.AppendLine($"{info.IsEnabled},{info.IPAddress},{info.Hostname},{info.Comment}");
53+
54+
File.WriteAllText(filePath, stringBuilder.ToString());
55+
}
56+
57+
/// <summary>
58+
/// Creates a XML file from the given <see cref="HostsFileEntry" /> collection.
59+
/// </summary>
60+
/// <param name="collection">Objects as <see cref="IReadOnlyList{HostsFileEntry}" /> to export.</param>
61+
/// <param name="filePath">Path to the export file.</param>
62+
private static void CreateXml(IEnumerable<HostsFileEntry> collection, string filePath)
63+
{
64+
var document = new XDocument(DefaultXDeclaration,
65+
new XElement(ApplicationName.HostsFileEditor.ToString(),
66+
new XElement(nameof(HostsFileEntry) + "s",
67+
from info in collection
68+
select
69+
new XElement(nameof(HostsFileEntry),
70+
new XElement(nameof(HostsFileEntry.IsEnabled), info.IsEnabled),
71+
new XElement(nameof(HostsFileEntry.IPAddress), info.IPAddress),
72+
new XElement(nameof(HostsFileEntry.Hostname), info.Hostname),
73+
new XElement(nameof(HostsFileEntry.Comment), info.Comment)))));
74+
75+
document.Save(filePath);
76+
}
77+
78+
/// <summary>
79+
/// Creates a JSON file from the given <see cref="HostsFileEntry" /> collection.
80+
/// </summary>
81+
/// <param name="collection">Objects as <see cref="IReadOnlyList{HostsFileEntry}" /> to export.</param>
82+
/// <param name="filePath">Path to the export file.</param>
83+
private static void CreateJson(IReadOnlyList<HostsFileEntry> collection, string filePath)
84+
{
85+
var jsonData = new object[collection.Count];
86+
87+
for (var i = 0; i < collection.Count; i++)
88+
jsonData[i] = new
89+
{
90+
collection[i].IsEnabled,
91+
collection[i].IPAddress,
92+
collection[i].Hostname,
93+
collection[i].Comment
94+
};
95+
96+
File.WriteAllText(filePath, JsonConvert.SerializeObject(jsonData, Formatting.Indented));
97+
}
98+
}

Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private static IEnumerable<HostsFileEntry> GetHostsFileEntries()
107107
IsEnabled = !result.Groups[1].Value.Equals("#"),
108108
IPAddress = result.Groups[2].Value,
109109
Hostname = result.Groups[3].Value.Replace(@"\s", "").Trim(),
110-
Comment = result.Groups[4].Value.TrimStart('#'),
110+
Comment = result.Groups[4].Value.TrimStart('#',' '),
111111
Line = line
112112
};
113113

Source/NETworkManager.Settings/GlobalStaticConfiguration.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ public static class GlobalStaticConfiguration
1919
{
2020
#region Global settings
2121

22+
// Application config
23+
public static int ApplicationUIRefreshInterval => 2500;
24+
2225
// Type to search (average type speed --> 187 chars/min)
2326
public static TimeSpan SearchDispatcherTimerTimeSpan => new(0, 0, 0, 0, 750);
2427

2528
// Network config
2629
public static int NetworkChangeDetectionDelay => 5000;
27-
30+
2831
// Profile config
2932
public static bool Profile_ExpandProfileView => true;
3033
public static double Profile_WidthCollapsed => 40;
@@ -219,6 +222,9 @@ public static class GlobalStaticConfiguration
219222
// Application: SNTP Lookup
220223
public static int SNTPLookup_Timeout => 4000;
221224
public static ExportFileType SNTPLookup_ExportFileType => ExportFileType.Csv;
225+
226+
// Application: Hosts File Editor
227+
public static ExportFileType HostsFileEditor_ExportFileType => ExportFileType.Csv;
222228

223229
// Application: Discovery Protocol
224230
public static DiscoveryProtocol DiscoveryProtocol_Protocol => DiscoveryProtocol.LldpCdp;

Source/NETworkManager.Settings/SettingsInfo.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3584,7 +3584,7 @@ public ExportFileType SNMP_ExportFileType
35843584

35853585
#region SNTP Lookup
35863586

3587-
private ObservableCollection<ServerConnectionInfoProfile> _sntpLookup_SNTPServers = new();
3587+
private ObservableCollection<ServerConnectionInfoProfile> _sntpLookup_SNTPServers = [];
35883588

35893589
public ObservableCollection<ServerConnectionInfoProfile> SNTPLookup_SNTPServers
35903590
{
@@ -3660,6 +3660,40 @@ public ExportFileType SNTPLookup_ExportFileType
36603660
}
36613661

36623662
#endregion
3663+
3664+
#region Hosts File Editor
3665+
3666+
private string _hostsFileEditor_ExportFilePath;
3667+
3668+
public string HostsFileEditor_ExportFilePath
3669+
{
3670+
get => _hostsFileEditor_ExportFilePath;
3671+
set
3672+
{
3673+
if (value == _hostsFileEditor_ExportFilePath)
3674+
return;
3675+
3676+
_hostsFileEditor_ExportFilePath = value;
3677+
OnPropertyChanged();
3678+
}
3679+
}
3680+
3681+
private ExportFileType _hostsFileEditor_ExportFileType = GlobalStaticConfiguration.HostsFileEditor_ExportFileType;
3682+
3683+
public ExportFileType HostsFileEditor_ExportFileType
3684+
{
3685+
get => _hostsFileEditor_ExportFileType;
3686+
set
3687+
{
3688+
if (value == _hostsFileEditor_ExportFileType)
3689+
return;
3690+
3691+
_hostsFileEditor_ExportFileType = value;
3692+
OnPropertyChanged();
3693+
}
3694+
}
3695+
3696+
#endregion
36633697

36643698
#region Discovery Protocol
36653699

Source/NETworkManager/ViewModels/AWSSessionManagerHostViewModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ private async Task SyncAllInstanceIDsFromAWS()
650650
}
651651

652652
// Make the user happy, let him see a reload animation (and he cannot spam the reload command)
653-
await Task.Delay(2500);
653+
await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval);
654654

655655
Log.Info("All Instance IDs synced from AWS!");
656656

@@ -679,7 +679,7 @@ private async Task SyncGroupInstanceIDsFromAWS(string group)
679679
}
680680

681681
// Make the user happy, let him see a reload animation (and he cannot spam the reload command)
682-
await Task.Delay(2500);
682+
await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval);
683683

684684
Log.Info("Group synced!");
685685

Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,6 @@ public HostsFileEditorViewModel(IDialogCoordinator instance)
172172
// Watch hosts file for changes
173173
HostsFileEditor.HostsFileChanged += (_, _) =>
174174
{
175-
StatusMessage = "Refreshing...";
176-
IsStatusMessageDisplayed = true;
177-
178175
Application.Current.Dispatcher.Invoke(() =>
179176
{
180177
Refresh().ConfigureAwait(false);
@@ -203,11 +200,54 @@ private bool Refresh_CanExecute(object parameter)
203200

204201
private async Task RefreshAction()
205202
{
206-
StatusMessage = "Refreshing...";
207-
IsStatusMessageDisplayed = true;
208-
209203
await Refresh();
210204
}
205+
206+
public ICommand ExportCommand => new RelayCommand(_ => ExportAction().ConfigureAwait(false));
207+
208+
private async Task ExportAction()
209+
{
210+
var customDialog = new CustomDialog
211+
{
212+
Title = Strings.Export
213+
};
214+
215+
var exportViewModel = new ExportViewModel(async instance =>
216+
{
217+
await _dialogCoordinator.HideMetroDialogAsync(this, customDialog);
218+
219+
try
220+
{
221+
ExportManager.Export(instance.FilePath, instance.FileType,
222+
instance.ExportAll
223+
? Results
224+
: new ObservableCollection<HostsFileEntry>(SelectedResults.Cast<HostsFileEntry>().ToArray()));
225+
}
226+
catch (Exception ex)
227+
{
228+
Log.Error("Error while exporting data as " + instance.FileType, ex);
229+
230+
var settings = AppearanceManager.MetroDialog;
231+
settings.AffirmativeButtonText = Strings.OK;
232+
233+
await _dialogCoordinator.ShowMessageAsync(this, Strings.Error,
234+
Strings.AnErrorOccurredWhileExportingTheData + Environment.NewLine +
235+
Environment.NewLine + ex.Message, MessageDialogStyle.Affirmative, settings);
236+
}
237+
238+
SettingsManager.Current.HostsFileEditor_ExportFileType = instance.FileType;
239+
SettingsManager.Current.HostsFileEditor_ExportFilePath = instance.FilePath;
240+
}, _ => { _dialogCoordinator.HideMetroDialogAsync(this, customDialog); }, [
241+
ExportFileType.Csv, ExportFileType.Xml, ExportFileType.Json
242+
], true, SettingsManager.Current.HostsFileEditor_ExportFileType, SettingsManager.Current.HostsFileEditor_ExportFilePath);
243+
244+
customDialog.Content = new ExportDialog
245+
{
246+
DataContext = exportViewModel
247+
};
248+
249+
await _dialogCoordinator.ShowMetroDialogAsync(this, customDialog);
250+
}
211251

212252
public ICommand RestartAsAdminCommand => new RelayCommand(_ => RestartAsAdminAction().ConfigureAwait(false));
213253

@@ -233,19 +273,47 @@ private async Task Refresh(bool init = false)
233273
return;
234274

235275
IsRefreshing = true;
276+
277+
// Retry 3 times if the hosts file is locked
278+
for (var i = 1; i < 4; i++)
279+
{
280+
// Wait for 2.5 seconds on refresh
281+
if (init == false || i > 1)
282+
{
283+
StatusMessage = "Refreshing...";
284+
IsStatusMessageDisplayed = true;
285+
286+
await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval);
287+
}
236288

237-
if (!init)
238-
await Task.Delay(2500);
289+
try
290+
{
291+
var entries = await HostsFileEditor.GetHostsFileEntriesAsync();
292+
293+
Results.Clear();
294+
295+
entries.ToList().ForEach(Results.Add);
296+
297+
StatusMessage = "Reloaded at " + DateTime.Now.ToShortTimeString();
298+
IsStatusMessageDisplayed = true;
299+
300+
break;
301+
}
302+
catch (Exception ex)
303+
{
304+
Log.Error(ex);
305+
306+
StatusMessage = "Failed to reload hosts file: " + ex.Message;
239307

240-
Results.Clear();
241-
242-
// Todo: try catch + Re-try count and delay
243-
244-
(await HostsFileEditor.GetHostsFileEntriesAsync()).ToList().ForEach(Results.Add);
245-
246-
StatusMessage = "Reloaded at " + DateTime.Now.ToShortTimeString();
247-
IsStatusMessageDisplayed = true;
308+
if (i < 3)
309+
StatusMessage += Environment.NewLine + "Retrying in 2.5 seconds...";
248310

311+
IsStatusMessageDisplayed = true;
312+
313+
await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval);
314+
}
315+
}
316+
249317
IsRefreshing = false;
250318
}
251319

Source/NETworkManager/ViewModels/IPApiDNSResolverWidgetViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ private async Task CheckAsync()
8888
Result = null;
8989

9090
// Make the user happy, let him see a reload animation (and he cannot spam the reload command)
91-
await Task.Delay(2500);
91+
await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval);
9292

9393
Result = await DNSResolverService.GetInstance().GetDNSResolverAsync();
9494

Source/NETworkManager/ViewModels/IPApiIPGeolocationWidgetViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ private async Task CheckAsync()
9090
Result = null;
9191

9292
// Make the user happy, let him see a reload animation (and he cannot spam the reload command)
93-
await Task.Delay(2500);
93+
await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval);
9494

9595
Result = await IPGeolocationService.GetInstance().GetIPGeolocationAsync();
9696

Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -938,16 +938,14 @@ private async Task RemoveIPv4AddressAction()
938938

939939
private async void ReloadNetworkInterfaces()
940940
{
941-
Debug.WriteLine("ReloadNetworkInterfaces.............");
942-
943941
// Avoid multiple reloads
944942
if(IsNetworkInterfaceLoading)
945943
return;
946944

947945
IsNetworkInterfaceLoading = true;
948946

949947
// Make the user happy, let him see a reload animation (and he cannot spam the reload command)
950-
await Task.Delay(2500);
948+
await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval);
951949

952950
// Store the last selected id
953951
var id = SelectedNetworkInterface?.Id ?? string.Empty;

Source/NETworkManager/ViewModels/SettingsAutostartViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ private async Task EnableDisableAutostart(bool enable)
4747
await AutostartManager.DisableAsync();
4848

4949
// Make the user happy, let him see a reload animation (and he cannot spam the reload command)
50-
await Task.Delay(2500);
50+
await Task.Delay(GlobalStaticConfiguration.ApplicationUIRefreshInterval);
5151
}
5252
catch (Exception ex)
5353
{

0 commit comments

Comments
 (0)