From cad8b7db9b358744eb996adf311d8d665984d880 Mon Sep 17 00:00:00 2001 From: laurentiu021 Date: Fri, 12 Jun 2026 18:19:16 +0300 Subject: [PATCH] fix: preserve all hostnames per IP and write hosts file atomically --- CHANGELOG.md | 6 ++- .../SysManager.Tests/HostsFileServiceTests.cs | 44 +++++++++++++++++++ .../SysManager/Services/HostsFileService.cs | 38 ++++++++++++---- SysManager/SysManager/SysManager.csproj | 6 +-- 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 208ff2f..539553b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -## [1.20.23] - 2026-06-12 +## [1.20.24] - 2026-06-12 + +### Fixed +- **Hosts entries with multiple hostnames per IP are no longer silently lost.** A line like `127.0.0.1 a b c` was read keeping only the first hostname, so editing and saving dropped the rest. Each hostname is now read as its own entry and survives a round trip. +- **The hosts file is now written atomically.** Saving wrote directly over the file, so a crash mid-write could leave it empty or truncated. It now writes to a temporary file and atomically replaces the target, cleaning up the temp file afterward. ### Fixed - **No more leaked process handles when opening Explorer / links.** Ten places that launch Explorer ("show in folder"/"open file location"), Event Viewer, the browser, or the updater left the returned process handle undisposed. Each now releases it. The launched program is unaffected; only the orphaned handle is cleaned up. Covers Deep Cleanup, Disk Analyzer, Duplicate Finder, Startup Manager, Logs, About, and the Context Menu refresh. diff --git a/SysManager/SysManager.Tests/HostsFileServiceTests.cs b/SysManager/SysManager.Tests/HostsFileServiceTests.cs index bd6a05f..8313482 100644 --- a/SysManager/SysManager.Tests/HostsFileServiceTests.cs +++ b/SysManager/SysManager.Tests/HostsFileServiceTests.cs @@ -93,4 +93,48 @@ public void RestoreBackup_NoBackup_ReturnsFalse() try { Directory.Delete(dir, recursive: true); } catch { } } } + + [Fact] + public async Task ReadHostsAsync_MultipleHostnamesPerIp_AllPreserved() + { + // Regression (data loss): a line mapping one IP to several hostnames + // ("127.0.0.1 a b c") previously kept only the first hostname, so the + // others were dropped on a read→save round trip. Each must survive as its + // own entry. + var (svc, _, dir) = NewServiceWithTempHosts("127.0.0.1\talpha beta gamma\n"); + try + { + var entries = await svc.ReadHostsAsync(); + var hosts = entries.Where(e => e.IpAddress == "127.0.0.1").Select(e => e.Hostname).ToList(); + Assert.Contains("alpha", hosts); + Assert.Contains("beta", hosts); + Assert.Contains("gamma", hosts); + } + finally + { + try { Directory.Delete(dir, recursive: true); } catch { } + } + } + + [Fact] + public void SaveHosts_LeavesNoTempFileBehind() + { + // Regression (atomic write): SaveHosts writes to a temp file then moves it + // into place. After a successful save no ".sysmanager.tmp" must remain. + var (svc, hosts, dir) = NewServiceWithTempHosts("127.0.0.1 localhost\n"); + try + { + svc.SaveHosts(new List + { + new() { IpAddress = "127.0.0.1", Hostname = "localhost", IsEnabled = true } + }); + Assert.False(File.Exists(hosts + ".sysmanager.tmp"), "temp file was left behind after save"); + Assert.True(File.Exists(hosts)); + Assert.Contains("localhost", File.ReadAllText(hosts)); + } + finally + { + try { Directory.Delete(dir, recursive: true); } catch { } + } + } } diff --git a/SysManager/SysManager/Services/HostsFileService.cs b/SysManager/SysManager/Services/HostsFileService.cs index 1eabfdb..d015a24 100644 --- a/SysManager/SysManager/Services/HostsFileService.cs +++ b/SysManager/SysManager/Services/HostsFileService.cs @@ -70,18 +70,23 @@ public async Task> ReadHostsAsync(CancellationToken ct = defaul if (tokens.Length < 2) continue; string ip = tokens[0]; - string hostname = tokens[1]; // Validate IP if (!IPAddress.TryParse(ip, out _)) continue; - entries.Add(new HostsEntry + // A single hosts line can map one IP to several hostnames + // (e.g. "127.0.0.1 a b c"). Emit one entry per hostname so none are + // silently dropped on a read→save round trip. + for (int t = 1; t < tokens.Length; t++) { - IpAddress = ip, - Hostname = hostname, - Comment = comment, - IsEnabled = !isDisabled - }); + entries.Add(new HostsEntry + { + IpAddress = ip, + Hostname = tokens[t], + Comment = comment, + IsEnabled = !isDisabled + }); + } } return entries; @@ -122,7 +127,24 @@ public void SaveHosts(List entries) lines.Add(line); } - File.WriteAllLines(HostsPath, lines); + // Write atomically: a crash midway through File.WriteAllLines would otherwise + // leave the hosts file truncated or empty. Write to a temp file in the same + // directory, then replace the target in one move. + var tempPath = HostsPath + ".sysmanager.tmp"; + try + { + File.WriteAllLines(tempPath, lines); + File.Move(tempPath, HostsPath, overwrite: true); + } + finally + { + if (File.Exists(tempPath)) + { + try { File.Delete(tempPath); } + catch (IOException) { /* best-effort temp cleanup */ } + catch (UnauthorizedAccessException) { /* best-effort temp cleanup */ } + } + } } /// diff --git a/SysManager/SysManager/SysManager.csproj b/SysManager/SysManager/SysManager.csproj index 29562f5..c682f33 100644 --- a/SysManager/SysManager/SysManager.csproj +++ b/SysManager/SysManager/SysManager.csproj @@ -10,9 +10,9 @@ SysManager true NU1603;NU1701 - 1.20.23 - 1.20.23.0 - 1.20.23.0 + 1.20.24 + 1.20.24.0 + 1.20.24.0 SysManager SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup. https://github.com/laurentiu021/SystemManager