Skip to content

Commit 6be5a35

Browse files
committed
Feature: Enable / disable entries
1 parent 4972ebd commit 6be5a35

File tree

2 files changed

+205
-26
lines changed

2 files changed

+205
-26
lines changed
Lines changed: 203 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,58 @@
1-
using System;
1+
using log4net;
2+
using NETworkManager.Utilities;
3+
using System;
24
using System.Collections.Generic;
35
using System.IO;
6+
using System.Linq;
47
using System.Text.RegularExpressions;
58
using System.Threading.Tasks;
6-
using log4net;
7-
using NETworkManager.Utilities;
89

910
namespace NETworkManager.Models.HostsFileEditor;
1011

1112
public static class HostsFileEditor
1213
{
13-
#region Events
14+
#region Events
15+
1416
public static event EventHandler HostsFileChanged;
15-
17+
1618
private static void OnHostsFileChanged()
1719
{
1820
Log.Debug("OnHostsFileChanged - Hosts file changed.");
1921
HostsFileChanged?.Invoke(null, EventArgs.Empty);
2022
}
23+
2124
#endregion
22-
25+
2326
#region Variables
27+
2428
private static readonly ILog Log = LogManager.GetLogger(typeof(HostsFileEditor));
2529

2630
private static readonly FileSystemWatcher HostsFileWatcher;
27-
31+
32+
/// <summary>
33+
/// Path to the hosts folder.
34+
/// </summary>
35+
private static string HostsFolderPath => Path.Combine(Environment.SystemDirectory, "drivers", "etc");
36+
2837
/// <summary>
2938
/// Path to the hosts file.
3039
/// </summary>
31-
private static string HostsFilePath => Path.Combine(Environment.SystemDirectory, "drivers", "etc", "hosts");
40+
private static string HostsFilePath => Path.Combine(HostsFolderPath, "hosts");
41+
42+
/// <summary>
43+
/// Identifier for the hosts file backup.
44+
/// </summary>
45+
private static string HostsFileBackupIdentifier => "hosts_backup_NETworkManager";
46+
47+
/// <summary>
48+
/// Number of backups to keep.
49+
/// </summary>
50+
private static int HostsFileBackupsToKeep => 5;
51+
52+
/// <summary>
53+
/// Last time a backup was created.
54+
/// </summary>
55+
private static DateTime _lastBackupTime = DateTime.MinValue;
3256

3357
/// <summary>
3458
/// Example values in the hosts file that should be ignored.
@@ -38,14 +62,14 @@ private static void OnHostsFileChanged()
3862
("102.54.94.97", "rhino.acme.com"),
3963
("38.25.63.10", "x.acme.com")
4064
];
41-
65+
4266
/// <summary>
4367
/// Regex to match a hosts file entry with optional comments, supporting IPv4, IPv6, and hostnames
4468
/// </summary>
4569
private static readonly Regex HostsFileEntryRegex = new(RegexHelper.HostsEntryRegex);
4670

4771
#endregion
48-
72+
4973
#region Constructor
5074

5175
static HostsFileEditor()
@@ -54,39 +78,47 @@ static HostsFileEditor()
5478
try
5579
{
5680
Log.Debug("HostsFileEditor - Creating file system watcher for hosts file...");
57-
81+
5882
// Create the file system watcher
5983
HostsFileWatcher = new FileSystemWatcher();
60-
HostsFileWatcher.Path = Path.GetDirectoryName(HostsFilePath) ?? throw new InvalidOperationException("Hosts file path is invalid.");
61-
HostsFileWatcher.Filter = Path.GetFileName(HostsFilePath) ?? throw new InvalidOperationException("Hosts file name is invalid.");
84+
HostsFileWatcher.Path = Path.GetDirectoryName(HostsFilePath) ??
85+
throw new InvalidOperationException("Hosts file path is invalid.");
86+
HostsFileWatcher.Filter = Path.GetFileName(HostsFilePath) ??
87+
throw new InvalidOperationException("Hosts file name is invalid.");
6288
HostsFileWatcher.NotifyFilter = NotifyFilters.LastWrite;
63-
89+
6490
// Maybe fired twice. This is a known bug/feature.
6591
// See: https://stackoverflow.com/questions/1764809/filesystemwatcher-changed-event-is-raised-twice
6692
HostsFileWatcher.Changed += (_, _) => OnHostsFileChanged();
67-
93+
6894
// Enable the file system watcher
6995
HostsFileWatcher.EnableRaisingEvents = true;
70-
96+
7197
Log.Debug("HostsFileEditor - File system watcher for hosts file created.");
7298
}
7399
catch (Exception ex)
74100
{
75101
Log.Error("Failed to create file system watcher for hosts file.", ex);
76102
}
77103
}
104+
78105
#endregion
79106

80107
#region Methods
108+
109+
/// <summary>
110+
/// Gets the entries from the hosts file asynchronously.
111+
/// </summary>
112+
/// <returns>A task that represents the asynchronous operation, containing all entries from the hosts file.</returns>
81113
public static Task<IEnumerable<HostsFileEntry>> GetHostsFileEntriesAsync()
82114
{
83115
return Task.Run(GetHostsFileEntries);
84116
}
85-
117+
86118
/// <summary>
87-
///
119+
/// Gets the entries from the hosts file.
88120
/// </summary>
89-
/// <returns></returns>
121+
/// <returns>All entries from the hosts file.</returns>
90122
private static IEnumerable<HostsFileEntry> GetHostsFileEntries()
91123
{
92124
var hostsFileLines = File.ReadAllLines(HostsFilePath);
@@ -101,26 +133,26 @@ private static IEnumerable<HostsFileEntry> GetHostsFileEntries()
101133
if (result.Success)
102134
{
103135
Log.Debug("GetHostsFileEntries - Line matched: " + line);
104-
136+
105137
var entry = new HostsFileEntry
106138
{
107139
IsEnabled = !result.Groups[1].Value.Equals("#"),
108140
IPAddress = result.Groups[2].Value,
109141
Hostname = result.Groups[3].Value.Replace(@"\s", "").Trim(),
110-
Comment = result.Groups[4].Value.TrimStart('#',' '),
142+
Comment = result.Groups[4].Value.TrimStart('#', ' '),
111143
Line = line
112144
};
113-
145+
114146
// Skip example entries
115-
if(!entry.IsEnabled)
147+
if (!entry.IsEnabled)
116148
{
117149
if (ExampleValuesToIgnore.Contains((entry.IPAddress, entry.Hostname)))
118150
{
119151
Log.Debug("GetHostsFileEntries - Matched example entry. Skipping...");
120152
continue;
121153
}
122154
}
123-
155+
124156
entries.Add(entry);
125157
}
126158
else
@@ -131,5 +163,152 @@ private static IEnumerable<HostsFileEntry> GetHostsFileEntries()
131163

132164
return entries;
133165
}
166+
167+
public static Task<bool> EnableEntryAsync(HostsFileEntry entry)
168+
{
169+
return Task.Run(() => EnableEntry(entry));
170+
}
171+
172+
private static bool EnableEntry(HostsFileEntry entry)
173+
{
174+
// Create a backup of the hosts file before making changes
175+
if (CreateBackup() == false)
176+
{
177+
Log.Error("EnableEntry - Failed to create backup before enabling entry.");
178+
return false;
179+
}
180+
181+
// Replace the entry in the hosts file
182+
var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList();
183+
184+
for (var i = 0; i < hostsFileLines.Count; i++)
185+
{
186+
if (hostsFileLines[i] == entry.Line)
187+
hostsFileLines[i] = entry.Line.TrimStart('#', ' ');
188+
}
189+
190+
try
191+
{
192+
Log.Debug($"EnableEntry - Writing changes to hosts file: {HostsFilePath}");
193+
File.WriteAllLines(HostsFilePath, hostsFileLines);
194+
}
195+
catch (Exception ex)
196+
{
197+
Log.Error($"EnableEntry - Failed to write changes to hosts file: {HostsFilePath}", ex);
198+
199+
return false;
200+
}
201+
202+
return true;
203+
}
204+
205+
public static Task<bool> DisableEntryAsync(HostsFileEntry entry)
206+
{
207+
return Task.Run(() => DisableEntry(entry));
208+
}
209+
210+
private static bool DisableEntry(HostsFileEntry entry)
211+
{
212+
// Create a backup of the hosts file before making changes
213+
if (CreateBackup() == false)
214+
{
215+
Log.Error("DisableEntry - Failed to create backup before disabling entry.");
216+
return false;
217+
}
218+
219+
// Replace the entry in the hosts file
220+
var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList();
221+
222+
for (var i = 0; i < hostsFileLines.Count; i++)
223+
{
224+
if (hostsFileLines[i] == entry.Line)
225+
hostsFileLines[i] = "# " + entry.Line;
226+
}
227+
228+
try
229+
{
230+
Log.Debug($"DisableEntry - Writing changes to hosts file: {HostsFilePath}");
231+
File.WriteAllLines(HostsFilePath, hostsFileLines);
232+
}
233+
catch (Exception ex)
234+
{
235+
Log.Error($"DisableEntry - Failed to write changes to hosts file: {HostsFilePath}", ex);
236+
237+
return false;
238+
}
239+
240+
return true;
241+
}
242+
243+
/// <summary>
244+
/// Create a daily backup of the hosts file (before making a change).
245+
/// </summary>
246+
private static bool CreateBackup()
247+
{
248+
Log.Debug($"CreateBackup - Creating backup of hosts file: {HostsFilePath}");
249+
250+
var dateTimeNow = DateTime.Now;
251+
252+
// Check if a daily backup has already been created today (in the current running instance)
253+
if (_lastBackupTime.Date == dateTimeNow.Date)
254+
{
255+
Log.Debug("CreateBackup - Daily backup already created today. Skipping...");
256+
return true;
257+
}
258+
259+
// Get existing backup files
260+
var backupFiles = Directory.GetFiles(HostsFolderPath, $"{HostsFileBackupIdentifier}_*")
261+
.OrderByDescending(f => f).ToList();
262+
263+
Log.Debug($"CreateBackup - Found {backupFiles.Count} backup files in {HostsFolderPath}");
264+
265+
// Cleanup old backups if they exceed the limit
266+
if (backupFiles.Count > HostsFileBackupsToKeep)
267+
{
268+
for (var i = HostsFileBackupsToKeep; i < backupFiles.Count; i++)
269+
{
270+
try
271+
{
272+
Log.Debug($"CreateBackup - Deleting old backup file: {backupFiles[i]}");
273+
File.Delete(backupFiles[i]);
274+
}
275+
catch (Exception ex)
276+
{
277+
Log.Error($"CreateBackup - Failed to delete old backup file: {backupFiles[i]}", ex);
278+
}
279+
}
280+
}
281+
282+
// Check if a daily backup already exists on disk (from previous instances)
283+
var dailyBackupFound = backupFiles.Count > 0 &&
284+
backupFiles[0].Contains($"{HostsFileBackupIdentifier}_{dateTimeNow:yyyyMMdd}");
285+
286+
if (dailyBackupFound)
287+
{
288+
Log.Debug("CreateBackup - Daily backup already exists on disk. Skipping...");
289+
290+
_lastBackupTime = dateTimeNow;
291+
292+
return true;
293+
}
294+
295+
// Create a new backup file with the current date
296+
try
297+
{
298+
Log.Debug($"CreateBackup - Creating new backup file: {HostsFileBackupIdentifier}_{dateTimeNow:yyyyMMdd}");
299+
File.Copy(HostsFilePath,
300+
Path.Combine(HostsFolderPath, $"{HostsFileBackupIdentifier}_{dateTimeNow:yyyyMMdd}"));
301+
302+
_lastBackupTime = dateTimeNow;
303+
}
304+
catch (Exception ex)
305+
{
306+
Log.Error($"CreateBackup - Failed to create backup file: {HostsFilePath}", ex);
307+
return false;
308+
}
309+
310+
return true;
311+
}
312+
134313
#endregion
135314
}

Source/NETworkManager/ViewModels/HostsFileEditorViewModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,14 +253,14 @@ await _dialogCoordinator.ShowMessageAsync(this, Strings.Error,
253253

254254
private async Task EnableEntryAction()
255255
{
256-
MessageBox.Show("Enable entry action is not implemented yet.", "Enable Entry", MessageBoxButton.OK, MessageBoxImage.Information);
256+
await HostsFileEditor.EnableEntryAsync(SelectedResult);
257257
}
258258

259259
public ICommand DisableEntryCommand => new RelayCommand(_ => DisableEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute);
260260

261261
private async Task DisableEntryAction()
262262
{
263-
MessageBox.Show("Disable entry action is not implemented yet.", "Disable Entry", MessageBoxButton.OK, MessageBoxImage.Information);
263+
await HostsFileEditor.DisableEntryAsync(SelectedResult);
264264
}
265265

266266
public ICommand AddEntryCommand => new RelayCommand(_ => AddEntryAction().ConfigureAwait(false), ModifyEntry_CanExecute);

0 commit comments

Comments
 (0)