1- using System ;
1+ using log4net ;
2+ using NETworkManager . Utilities ;
3+ using System ;
24using System . Collections . Generic ;
35using System . IO ;
6+ using System . Linq ;
47using System . Text . RegularExpressions ;
58using System . Threading . Tasks ;
6- using log4net ;
7- using NETworkManager . Utilities ;
89
910namespace NETworkManager . Models . HostsFileEditor ;
1011
1112public 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}
0 commit comments