Skip to content

Commit cce3b95

Browse files
authored
Feature: HostsFileEditor add/edit (#3098)
* Feature: HostsFileEditor add/edit * Feature: Add & edit hosts file entry * Update next-release.md * Feature: Error message dialog * Update Strings.resx * Fix: Messages
1 parent 903b444 commit cce3b95

32 files changed

+2275
-1408
lines changed

Source/NETworkManager.Localization/Resources/Strings.Designer.cs

Lines changed: 1341 additions & 1288 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Source/NETworkManager.Localization/Resources/Strings.resx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@
679679
<value>Apply</value>
680680
</data>
681681
<data name="ApplyWindowsKeyCombinationsLikeAltTab" xml:space="preserve">
682-
<value>Apply Windows key combinations (e.g. ALT+TAB):</value>
682+
<value>Apply Windows key combinations (e.g., ALT+TAB):</value>
683683
</data>
684684
<data name="Auth" xml:space="preserve">
685685
<value>Auth</value>
@@ -1367,16 +1367,16 @@ Profile files are not affected!</value>
13671367
<value>Enter a valid IPv4 address!</value>
13681368
</data>
13691369
<data name="EnterValidMACAddress" xml:space="preserve">
1370-
<value>Enter a valid MAC address (like 00:F1:23:AB:F2:35)!</value>
1370+
<value>Enter a valid MAC address (e.g., 00:F1:23:AB:F2:35)!</value>
13711371
</data>
13721372
<data name="EnterValidPort" xml:space="preserve">
13731373
<value>Enter a valid port (1 - 65535)!</value>
13741374
</data>
13751375
<data name="EnterValidSubnetmask" xml:space="preserve">
1376-
<value>Enter a valid subnetmask (like 255.255.255.0)!</value>
1376+
<value>Enter a valid subnetmask (e.g., 255.255.255.0)!</value>
13771377
</data>
13781378
<data name="EnterValidSubnetmaskOrCIDR" xml:space="preserve">
1379-
<value>Enter a valid subnetmask or CIDR (like 255.255.255.0 or /24)!</value>
1379+
<value>Enter a valid subnetmask or CIDR (e.g., 255.255.255.0 or /24)!</value>
13801380
</data>
13811381
<data name="FieldCannotBeEmpty" xml:space="preserve">
13821382
<value>Field cannot be empty!</value>
@@ -1448,10 +1448,10 @@ Profile files are not affected!</value>
14481448
<value>Enter a valid port and/or port range (1 - 65535)!</value>
14491449
</data>
14501450
<data name="EnterValidSubnet" xml:space="preserve">
1451-
<value>Enter a valid subnet (like 192.168.178.133/26)!</value>
1451+
<value>Enter a valid subnet (e.g., 192.168.178.133/26)!</value>
14521452
</data>
14531453
<data name="EnterValidWebsiteUri" xml:space="preserve">
1454-
<value>Enter a valid website (like https://example.com/index.html)</value>
1454+
<value>Enter a valid website (e.g., https://example.com/index.html)</value>
14551455
</data>
14561456
<data name="Green" xml:space="preserve">
14571457
<value>Green</value>
@@ -1742,7 +1742,7 @@ Profile files are not affected!</value>
17421742
<value>Add a tab to query whois...</value>
17431743
</data>
17441744
<data name="EnterValidDomain" xml:space="preserve">
1745-
<value>Enter a valid domain (like "example.com")!</value>
1745+
<value>Enter a valid domain (e.g., "example.com")!</value>
17461746
</data>
17471747
<data name="WhoisServerNotFoundForTheDomain" xml:space="preserve">
17481748
<value>Whois server not found for the domain: "{0}"</value>
@@ -2032,7 +2032,7 @@ is disabled!</value>
20322032
<value>Use custom IPv4 address API</value>
20332033
</data>
20342034
<data name="HelpMessage_CustomPublicIPv4AddressAPI" xml:space="preserve">
2035-
<value>URL to a web service that can be reached via http or https and returns an IPv4 address like "xx.xx.xx.xx" as response.</value>
2035+
<value>URL to a web service that can be reached via http or https and returns an IPv4 address e.g., "xx.xx.xx.xx" as response.</value>
20362036
</data>
20372037
<data name="CouldNotParsePublicIPAddressFromXXXMessage" xml:space="preserve">
20382038
<value>Could not parse public ip address from "{0}"! Try another service or use the default... </value>
@@ -3192,7 +3192,7 @@ If the option is disabled again, the values are no longer modified. However, the
31923192
<value>SNTP server(s)</value>
31933193
</data>
31943194
<data name="HelpMessage_CustomPublicIPv6AddressAPI" xml:space="preserve">
3195-
<value>URL to a web service that can be reached via http or https and returns an IPv6 address like "xxxx:xx:xxx::xx" as response.</value>
3195+
<value>URL to a web service that can be reached via http or https and returns an IPv6 address e.g., "xxxx:xx:xxx::xx" as response.</value>
31963196
</data>
31973197
<data name="IPEndPoint" xml:space="preserve">
31983198
<value>IP endpoint</value>
@@ -3234,7 +3234,7 @@ If the option is disabled again, the values are no longer modified. However, the
32343234
<value>An SNTP server with this name already exists!</value>
32353235
</data>
32363236
<data name="EnterValidHostnameOrIPAddress" xml:space="preserve">
3237-
<value>Enter a valid hostname (like "server-01" or "example.com") or a valid IP address (like 192.168.178.1)!</value>
3237+
<value>Enter a valid hostname (e.g., "server-01" or "example.com") or a valid IP address (e.g., 192.168.178.1)!</value>
32383238
</data>
32393239
<data name="Servers" xml:space="preserve">
32403240
<value>Server(s)</value>
@@ -3528,10 +3528,10 @@ Changes to this value will take effect after the application is restarted. Wheth
35283528
<value>The settings on this page contain errors. Correct them to be able to save.</value>
35293529
</data>
35303530
<data name="HelpMessage_PuTTYHostkey" xml:space="preserve">
3531-
<value>SSH hostkey to use for the connection (like "71:b8:f2:6e..."). Only available if the mode is "SSH".</value>
3531+
<value>SSH hostkey to use for the connection (e.g., "71:b8:f2:6e..."). Only available if the mode is "SSH".</value>
35323532
</data>
35333533
<data name="HelpMessage_PuTTYPrivateKeyFile" xml:space="preserve">
3534-
<value>Full path to the private key file (like "C:\Users\BornToBeRoot\SSH\private_key.ppk"). Only available if the mode is "SSH".</value>
3534+
<value>Full path to the private key file (e.g., "C:\Users\BornToBeRoot\SSH\private_key.ppk"). Only available if the mode is "SSH".</value>
35353535
</data>
35363536
<data name="HelpMessage_PuTTYUsername" xml:space="preserve">
35373537
<value>Username that will be passed into the PuTTY session. Only available if the mode is "SSH", "Telnet" or "Rlogin".</value>
@@ -3938,4 +3938,22 @@ Right-click for more options.</value>
39383938

39393939
{0} {1} {2}</value>
39403940
</data>
3941+
<data name="ExampleHostsFileEntryComment" xml:space="preserve">
3942+
<value>Primary DNS server</value>
3943+
</data>
3944+
<data name="EnterValidHostsFileEntryHostname" xml:space="preserve">
3945+
<value>Enter a valid hostname (e.g., "server-01" or "example.com")! Multiple hostnames can be separated with a space.</value>
3946+
</data>
3947+
<data name="HostsFileReadErrorMessage" xml:space="preserve">
3948+
<value>The "hosts" file could not be read! See log file for more details.</value>
3949+
</data>
3950+
<data name="HostsFileWriteErrorMessage" xml:space="preserve">
3951+
<value>The "hosts" file could not be written to. See log file for more details.</value>
3952+
</data>
3953+
<data name="HostsFileEntryNotFoundMessage" xml:space="preserve">
3954+
<value>The entry was not found in the "hosts" file! Maybe the file has been modified.</value>
3955+
</data>
3956+
<data name="HostsFileBackupErrorMessage" xml:space="preserve">
3957+
<value>A backup of the "hosts" file could not be created! See log file for more details.</value>
3958+
</data>
39413959
</root>

Source/NETworkManager.Models/HostsFileEditor/HostsFileEditor.cs

Lines changed: 200 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,18 @@ private static HostsFileEntryModifyResult EnableEntry(HostsFileEntry entry)
188188
return HostsFileEntryModifyResult.BackupError;
189189
}
190190

191-
// Replace the entry in the hosts file
192-
var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList();
191+
// Enable the entry in the hosts file
192+
List<string> hostsFileLines;
193+
194+
try
195+
{
196+
hostsFileLines = [.. File.ReadAllLines(HostsFilePath)];
197+
}
198+
catch (Exception ex)
199+
{
200+
Log.Error($"EnableEntry - Failed to read hosts file: {HostsFilePath}", ex);
201+
return HostsFileEntryModifyResult.ReadError;
202+
}
193203

194204
bool entryFound = false;
195205

@@ -199,7 +209,15 @@ private static HostsFileEntryModifyResult EnableEntry(HostsFileEntry entry)
199209
{
200210
entryFound = true;
201211

202-
hostsFileLines[i] = entry.Line.TrimStart('#', ' ');
212+
hostsFileLines.RemoveAt(i);
213+
hostsFileLines.Insert(i, CreateEntryLine(new HostsFileEntry
214+
{
215+
IsEnabled = true,
216+
IPAddress = entry.IPAddress,
217+
Hostname = entry.Hostname,
218+
Comment = entry.Comment,
219+
Line = entry.Line
220+
}));
203221

204222
break;
205223
}
@@ -250,8 +268,18 @@ private static HostsFileEntryModifyResult DisableEntry(HostsFileEntry entry)
250268
return HostsFileEntryModifyResult.BackupError;
251269
}
252270

253-
// Replace the entry in the hosts file
254-
var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList();
271+
// Disable the entry in the hosts file
272+
List<string> hostsFileLines;
273+
274+
try
275+
{
276+
hostsFileLines = [.. File.ReadAllLines(HostsFilePath)];
277+
}
278+
catch (Exception ex)
279+
{
280+
Log.Error($"DisableEntry - Failed to read hosts file: {HostsFilePath}", ex);
281+
return HostsFileEntryModifyResult.ReadError;
282+
}
255283

256284
bool entryFound = false;
257285

@@ -261,7 +289,15 @@ private static HostsFileEntryModifyResult DisableEntry(HostsFileEntry entry)
261289
{
262290
entryFound = true;
263291

264-
hostsFileLines[i] = "# " + entry.Line;
292+
hostsFileLines.RemoveAt(i);
293+
hostsFileLines.Insert(i, CreateEntryLine(new HostsFileEntry
294+
{
295+
IsEnabled = false,
296+
IPAddress = entry.IPAddress,
297+
Hostname = entry.Hostname,
298+
Comment = entry.Comment,
299+
Line = entry.Line
300+
}));
265301

266302
break;
267303
}
@@ -287,6 +323,133 @@ private static HostsFileEntryModifyResult DisableEntry(HostsFileEntry entry)
287323
return HostsFileEntryModifyResult.Success;
288324
}
289325

326+
/// <summary>
327+
/// Add a hosts file entry asynchronously.
328+
/// </summary>
329+
/// <param name="entry">Entry to add.</param>
330+
/// <returns><see cref="HostsFileEntryModifyResult.Success"/> if the entry was added successfully, otherwise an error result.</returns>"/>
331+
public static Task<HostsFileEntryModifyResult> AddEntryAsync(HostsFileEntry entry)
332+
{
333+
return Task.Run(() => AddEntry(entry));
334+
}
335+
336+
/// <summary>
337+
/// Add a hosts file entry.
338+
/// </summary>
339+
/// <param name="entry">Entry to add.</param>
340+
/// <returns><see cref="HostsFileEntryModifyResult.Success"/> if the entry was added successfully, otherwise an error result.</returns>"/>
341+
private static HostsFileEntryModifyResult AddEntry(HostsFileEntry entry)
342+
{
343+
// Create a backup of the hosts file before making changes
344+
if (CreateBackup() == false)
345+
{
346+
Log.Error("AddEntry - Failed to create backup before adding entry.");
347+
return HostsFileEntryModifyResult.BackupError;
348+
}
349+
350+
// Add the entry to the hosts file
351+
List<string> hostsFileLines;
352+
353+
try
354+
{
355+
hostsFileLines = [.. File.ReadAllLines(HostsFilePath)];
356+
}
357+
catch (Exception ex)
358+
{
359+
Log.Error($"AddEntry - Failed to read hosts file: {HostsFilePath}", ex);
360+
return HostsFileEntryModifyResult.ReadError;
361+
}
362+
363+
hostsFileLines.Add(CreateEntryLine(entry));
364+
365+
try
366+
{
367+
Log.Debug($"AddEntry - Writing changes to hosts file: {HostsFilePath}");
368+
File.WriteAllLines(HostsFilePath, hostsFileLines);
369+
}
370+
catch (Exception ex)
371+
{
372+
Log.Error($"AddEntry - Failed to write changes to hosts file: {HostsFilePath}", ex);
373+
return HostsFileEntryModifyResult.WriteError;
374+
}
375+
376+
return HostsFileEntryModifyResult.Success;
377+
}
378+
379+
/// <summary>
380+
/// Edit a hosts file entry asynchronously.
381+
/// </summary>
382+
/// <param name="entry">Entry to edit.</param>
383+
/// <param name="newEntry">New entry to replace the old one.</param>
384+
/// <returns><see cref="HostsFileEntryModifyResult.Success"/> if the entry was edited successfully, otherwise an error result.</returns>"/>
385+
public static Task<HostsFileEntryModifyResult> EditEntryAsync(HostsFileEntry entry, HostsFileEntry newEntry)
386+
{
387+
return Task.Run(() => EditEntry(entry, newEntry));
388+
}
389+
390+
/// <summary>
391+
/// Edit a hosts file entry.
392+
/// </summary>
393+
/// <param name="entry">Entry to edit.</param>
394+
/// <param name="newEntry">New entry to replace the old one.</param>
395+
/// <returns><see cref="HostsFileEntryModifyResult.Success"/> if the entry was edited successfully, otherwise an error result.</returns>"/>
396+
private static HostsFileEntryModifyResult EditEntry(HostsFileEntry entry, HostsFileEntry newEntry)
397+
{
398+
// Create a backup of the hosts file before making changes
399+
if (CreateBackup() == false)
400+
{
401+
Log.Error("EditEntry - Failed to create backup before editing entry.");
402+
return HostsFileEntryModifyResult.BackupError;
403+
}
404+
405+
// Replace the entry from the hosts file
406+
List<string> hostsFileLines;
407+
408+
try
409+
{
410+
hostsFileLines = [.. File.ReadAllLines(HostsFilePath)];
411+
}
412+
catch (Exception ex)
413+
{
414+
Log.Error($"EditEntry - Failed to read hosts file: {HostsFilePath}", ex);
415+
return HostsFileEntryModifyResult.ReadError;
416+
}
417+
418+
bool entryFound = false;
419+
420+
for (var i = 0; i < hostsFileLines.Count; i++)
421+
{
422+
if (hostsFileLines[i] == entry.Line)
423+
{
424+
entryFound = true;
425+
426+
hostsFileLines.RemoveAt(i);
427+
hostsFileLines.Insert(i, CreateEntryLine(newEntry));
428+
429+
break;
430+
}
431+
}
432+
433+
if (!entryFound)
434+
{
435+
Log.Warn($"EditEntry - Entry not found in hosts file: {entry.Line}");
436+
return HostsFileEntryModifyResult.NotFound;
437+
}
438+
439+
try
440+
{
441+
Log.Debug($"EditEntry - Writing changes to hosts file: {HostsFilePath}");
442+
File.WriteAllLines(HostsFilePath, hostsFileLines);
443+
}
444+
catch (Exception ex)
445+
{
446+
Log.Error($"EditEntry - Failed to write changes to hosts file: {HostsFilePath}", ex);
447+
return HostsFileEntryModifyResult.WriteError;
448+
}
449+
450+
return HostsFileEntryModifyResult.Success;
451+
}
452+
290453
/// <summary>
291454
/// Delete a hosts file entry asynchronously.
292455
/// </summary>
@@ -312,7 +475,17 @@ private static HostsFileEntryModifyResult DeleteEntry(HostsFileEntry entry)
312475
}
313476

314477
// Remove the entry from the hosts file
315-
var hostsFileLines = File.ReadAllLines(HostsFilePath).ToList();
478+
List<string> hostsFileLines;
479+
480+
try
481+
{
482+
hostsFileLines = [.. File.ReadAllLines(HostsFilePath)];
483+
}
484+
catch (Exception ex)
485+
{
486+
Log.Error($"DeleteEntry - Failed to read hosts file: {HostsFilePath}", ex);
487+
return HostsFileEntryModifyResult.ReadError;
488+
}
316489

317490
bool entryFound = false;
318491

@@ -344,10 +517,29 @@ private static HostsFileEntryModifyResult DeleteEntry(HostsFileEntry entry)
344517
Log.Error($"DeleteEntry - Failed to write changes to hosts file: {HostsFilePath}", ex);
345518
return HostsFileEntryModifyResult.WriteError;
346519
}
347-
OnHostsFileChanged();
520+
348521
return HostsFileEntryModifyResult.Success;
349522
}
350523

524+
/// <summary>
525+
/// Create a line for the hosts file entry.
526+
/// </summary>
527+
/// <param name="entry">Entry to create the line for.</param>
528+
/// <returns>Line for the hosts file entry.</returns>
529+
private static string CreateEntryLine(HostsFileEntry entry)
530+
{
531+
var line = entry.IsEnabled ? "" : "# ";
532+
533+
line += $"{entry.IPAddress} {entry.Hostname}";
534+
535+
if (!string.IsNullOrWhiteSpace(entry.Comment))
536+
{
537+
line += $" # {entry.Comment}";
538+
}
539+
540+
return line.Trim();
541+
}
542+
351543
/// <summary>
352544
/// Create a "daily" backup of the hosts file (before making a change).
353545
/// </summary>

Source/NETworkManager.Models/HostsFileEditor/HostsFileEntryModifyResult.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ public enum HostsFileEntryModifyResult
1515
/// </summary>
1616
NotFound,
1717

18+
/// <summary>
19+
/// An error occurred while reading the hosts file.
20+
/// </summary>
21+
ReadError,
22+
1823
/// <summary>
1924
/// An error occurred while writing to the hosts file.
2025
/// </summary>

0 commit comments

Comments
 (0)