Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
44 changes: 44 additions & 0 deletions SysManager/SysManager.Tests/HostsFileServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HostsEntry>
{
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 { }
}
}
}
38 changes: 30 additions & 8 deletions SysManager/SysManager/Services/HostsFileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,23 @@ public async Task<List<HostsEntry>> 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;
Expand Down Expand Up @@ -122,7 +127,24 @@ public void SaveHosts(List<HostsEntry> 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 */ }
}
}
}

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions SysManager/SysManager/SysManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<RootNamespace>SysManager</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>NU1603;NU1701</NoWarn>
<Version>1.20.23</Version>
<FileVersion>1.20.23.0</FileVersion>
<AssemblyVersion>1.20.23.0</AssemblyVersion>
<Version>1.20.24</Version>
<FileVersion>1.20.24.0</FileVersion>
<AssemblyVersion>1.20.24.0</AssemblyVersion>
<Product>SysManager</Product>
<Description>SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup.</Description>
<PackageProjectUrl>https://github.com/laurentiu021/SystemManager</PackageProjectUrl>
Expand Down
Loading