mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-04 04:18:13 +10:00
Add config set and hosts caches; DnsmasqConfKeys; DHCP hosts from snapshot
- Config set cache: ConfigSetCache, IConfigSetCache, ConfigSetSnapshot. One read of main + conf-dir + managed config, build snapshot via parser pathToLines overloads. Watchers (main + managed file), 120s staleness, Invalidate(), NotifyWeWroteManagedConfig. - Parser: use DnsmasqConfKeys in DnsmasqConfIncludeParser and ConfigSetCache; add DnsmasqConfKeys.cs and use in DnsmasqConfOptionRegistry. - ConfigSetSnapshot: add DhcpHostEntries (built from pathToLines). ReadDhcpHostsAsync and ReadManagedConfigAsync use cache; writes call NotifyWeWroteManagedConfig. - DnsmasqConfigSetService: use IConfigSetCache only; all data from snapshot. - Hosts cache: HostsCache, IHostsCache, HostsSnapshot (managed + read-only). Watchers on managed and system hosts path, staleness, NotifyWeWroteManagedHosts. - HostsFileService and HostsController: use IHostsCache for ReadAsync and GetReadOnly. - Tests: DnsmasqConfigSetServiceTests use ConfigSetCache with test options.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Services;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
@@ -14,7 +15,8 @@ public class DnsmasqConfigSetServiceTests
|
||||
public void GetEffectiveConfigWithSources_NoMainPath_ReturnsDefaultConfigAndDefaultSources()
|
||||
{
|
||||
var options = Options.Create(new DnsmasqOptions { MainConfigPath = "" });
|
||||
var service = new DnsmasqConfigSetService(options);
|
||||
var cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||
var service = new DnsmasqConfigSetService(cache);
|
||||
var (config, sources) = service.GetEffectiveConfigWithSources();
|
||||
|
||||
Assert.NotNull(config);
|
||||
@@ -35,7 +37,7 @@ public class DnsmasqConfigSetServiceTests
|
||||
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-svc-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||
var managedPath = Path.Combine(dir, "zz-dnsmasq-webui.conf");
|
||||
ConfigSetCache? cache = null;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(mainPath, "port=53\ncache-size=500\naddn-hosts=/etc/hosts.extra\n");
|
||||
@@ -44,7 +46,8 @@ public class DnsmasqConfigSetServiceTests
|
||||
MainConfigPath = mainPath,
|
||||
ManagedFileName = "zz-dnsmasq-webui.conf"
|
||||
});
|
||||
var service = new DnsmasqConfigSetService(options);
|
||||
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||
var service = new DnsmasqConfigSetService(cache);
|
||||
var (config, sources) = service.GetEffectiveConfigWithSources();
|
||||
|
||||
Assert.NotNull(config);
|
||||
@@ -66,6 +69,7 @@ public class DnsmasqConfigSetServiceTests
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache?.Dispose();
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
using System.Text;
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Controllers;
|
||||
|
||||
@@ -16,15 +11,13 @@ public class HostsController : ControllerBase
|
||||
{
|
||||
private readonly IHostsFileService _hostsService;
|
||||
private readonly IReloadService _reloadService;
|
||||
private readonly IDnsmasqConfigSetService _configSetService;
|
||||
private readonly DnsmasqOptions _options;
|
||||
private readonly IHostsCache _hostsCache;
|
||||
|
||||
public HostsController(IHostsFileService hostsService, IReloadService reloadService, IDnsmasqConfigSetService configSetService, IOptions<DnsmasqOptions> options)
|
||||
public HostsController(IHostsFileService hostsService, IReloadService reloadService, IHostsCache hostsCache)
|
||||
{
|
||||
_hostsService = hostsService;
|
||||
_reloadService = reloadService;
|
||||
_configSetService = configSetService;
|
||||
_options = options.Value;
|
||||
_hostsCache = hostsCache;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -47,68 +40,8 @@ public class HostsController : ControllerBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var set = await _configSetService.GetConfigSetAsync(ct);
|
||||
var effectiveConfig = _configSetService.GetEffectiveConfig();
|
||||
var addnPaths = effectiveConfig.AddnHostsPaths ?? Array.Empty<string>();
|
||||
var managedPath = set.ManagedHostsFilePath != null ? Path.GetFullPath(set.ManagedHostsFilePath) : null;
|
||||
|
||||
var result = new List<ReadOnlyHostsFile>();
|
||||
|
||||
// System hosts: only when configured and no-hosts is false (dnsmasq reads it then).
|
||||
var systemPath = _options.SystemHostsPath?.Trim();
|
||||
if (!string.IsNullOrEmpty(systemPath) && !effectiveConfig.NoHosts)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(systemPath);
|
||||
if (System.IO.File.Exists(fullPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var lines = await System.IO.File.ReadAllLinesAsync(fullPath, Encoding.UTF8, ct);
|
||||
var entries = new List<HostEntry>();
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var entry = HostsFileLineParser.ParseLine(lines[i], i + 1);
|
||||
if (entry != null)
|
||||
entries.Add(entry);
|
||||
}
|
||||
result.Add(new ReadOnlyHostsFile(fullPath, entries));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip unreadable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var systemPathFull = !string.IsNullOrEmpty(systemPath) ? Path.GetFullPath(systemPath) : null;
|
||||
|
||||
foreach (var p in addnPaths)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(p);
|
||||
if (managedPath != null && string.Equals(fullPath, managedPath, StringComparison.Ordinal))
|
||||
continue;
|
||||
if (systemPathFull != null && string.Equals(fullPath, systemPathFull, StringComparison.Ordinal))
|
||||
continue;
|
||||
if (!System.IO.File.Exists(fullPath))
|
||||
continue;
|
||||
try
|
||||
{
|
||||
var lines = await System.IO.File.ReadAllLinesAsync(fullPath, Encoding.UTF8, ct);
|
||||
var entries = new List<HostEntry>();
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var entry = HostsFileLineParser.ParseLine(lines[i], i + 1);
|
||||
if (entry != null)
|
||||
entries.Add(entry);
|
||||
}
|
||||
result.Add(new ReadOnlyHostsFile(fullPath, entries));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip unreadable files
|
||||
}
|
||||
}
|
||||
return Ok(result);
|
||||
var snapshot = await _hostsCache.GetSnapshotAsync(ct);
|
||||
return Ok(snapshot.ReadOnlyFiles);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace DnsmasqWebUI.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Literal dnsmasq .conf option names (config keys). Use these everywhere we reference option names
|
||||
/// so references are compile-time checked and easy to find/rename. Values match dnsmasq long-option
|
||||
/// names without the leading "--". Case-sensitive per dnsmasq.
|
||||
/// </summary>
|
||||
public static class DnsmasqConfKeys
|
||||
{
|
||||
// --- Include (main config discovery) ---
|
||||
public const string ConfFile = "conf-file";
|
||||
public const string ConfDir = "conf-dir";
|
||||
|
||||
// --- Hosts ---
|
||||
public const string NoHosts = "no-hosts";
|
||||
public const string AddnHosts = "addn-hosts";
|
||||
|
||||
// --- DHCP lease file (last wins) ---
|
||||
public const string DhcpLeasefile = "dhcp-leasefile";
|
||||
public const string DhcpLease = "dhcp-lease";
|
||||
|
||||
// --- Multi-value (ARG_DUP) ---
|
||||
public const string Server = "server";
|
||||
public const string Local = "local";
|
||||
public const string Address = "address";
|
||||
public const string Interface = "interface";
|
||||
public const string ListenAddress = "listen-address";
|
||||
public const string ExceptInterface = "except-interface";
|
||||
public const string DhcpRange = "dhcp-range";
|
||||
public const string DhcpHost = "dhcp-host";
|
||||
public const string DhcpOption = "dhcp-option";
|
||||
public const string ResolvFile = "resolv-file";
|
||||
|
||||
// --- Flags (no value) ---
|
||||
public const string ExpandHosts = "expand-hosts";
|
||||
public const string BogusPriv = "bogus-priv";
|
||||
public const string StrictOrder = "strict-order";
|
||||
public const string NoResolv = "no-resolv";
|
||||
public const string DomainNeeded = "domain-needed";
|
||||
public const string NoPoll = "no-poll";
|
||||
public const string BindInterfaces = "bind-interfaces";
|
||||
public const string NoNegcache = "no-negcache";
|
||||
public const string DhcpAuthoritative = "dhcp-authoritative";
|
||||
public const string LeasefileRo = "leasefile-ro";
|
||||
|
||||
// --- Single-value (last wins) ---
|
||||
public const string CacheSize = "cache-size";
|
||||
public const string Port = "port";
|
||||
public const string LocalTtl = "local-ttl";
|
||||
public const string PidFile = "pid-file";
|
||||
public const string User = "user";
|
||||
public const string Group = "group";
|
||||
public const string LogFacility = "log-facility";
|
||||
public const string DhcpLeaseMax = "dhcp-lease-max";
|
||||
public const string NegTtl = "neg-ttl";
|
||||
public const string MaxTtl = "max-ttl";
|
||||
public const string MaxCacheTtl = "max-cache-ttl";
|
||||
public const string MinCacheTtl = "min-cache-ttl";
|
||||
public const string DhcpTtl = "dhcp-ttl";
|
||||
|
||||
/// <summary>Keys collected for effective config "server/local" multi-value (order preserved).</summary>
|
||||
public static readonly string[] ServerLocalKeys = { Server, Local };
|
||||
}
|
||||
@@ -12,52 +12,52 @@ public static class DnsmasqConfOptionRegistry
|
||||
new Dictionary<string, DnsmasqOptionKind>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Include
|
||||
{ "conf-file", DnsmasqOptionKind.ConfFile },
|
||||
{ "conf-dir", DnsmasqOptionKind.ConfDir },
|
||||
{ DnsmasqConfKeys.ConfFile, DnsmasqOptionKind.ConfFile },
|
||||
{ DnsmasqConfKeys.ConfDir, DnsmasqOptionKind.ConfDir },
|
||||
// Path-valued
|
||||
{ "addn-hosts", DnsmasqOptionKind.AddnHosts },
|
||||
{ "dhcp-leasefile", DnsmasqOptionKind.DhcpLeaseFile },
|
||||
{ "dhcp-lease", DnsmasqOptionKind.DhcpLeaseFile },
|
||||
{ "resolv-file", DnsmasqOptionKind.Path },
|
||||
{ DnsmasqConfKeys.AddnHosts, DnsmasqOptionKind.AddnHosts },
|
||||
{ DnsmasqConfKeys.DhcpLeasefile, DnsmasqOptionKind.DhcpLeaseFile },
|
||||
{ DnsmasqConfKeys.DhcpLease, DnsmasqOptionKind.DhcpLeaseFile },
|
||||
{ DnsmasqConfKeys.ResolvFile, DnsmasqOptionKind.Path },
|
||||
{ "dhcp-hostsfile", DnsmasqOptionKind.Path },
|
||||
{ "dhcp-optsfile", DnsmasqOptionKind.Path },
|
||||
{ "pid-file", DnsmasqOptionKind.Path },
|
||||
{ DnsmasqConfKeys.PidFile, DnsmasqOptionKind.Path },
|
||||
{ "hostsdir", DnsmasqOptionKind.Path },
|
||||
{ "dhcp-script", DnsmasqOptionKind.Path },
|
||||
{ "dhcp-lua-script", DnsmasqOptionKind.Path },
|
||||
{ "read-ethers", DnsmasqOptionKind.Path },
|
||||
// Simple string / domain
|
||||
{ "domain", DnsmasqOptionKind.Domain },
|
||||
{ "interface", DnsmasqOptionKind.String },
|
||||
{ "listen-address", DnsmasqOptionKind.String },
|
||||
{ "port", DnsmasqOptionKind.String },
|
||||
{ "user", DnsmasqOptionKind.String },
|
||||
{ "group", DnsmasqOptionKind.String },
|
||||
{ "cache-size", DnsmasqOptionKind.String },
|
||||
{ "local-ttl", DnsmasqOptionKind.String },
|
||||
{ "log-facility", DnsmasqOptionKind.String },
|
||||
{ DnsmasqConfKeys.Interface, DnsmasqOptionKind.String },
|
||||
{ DnsmasqConfKeys.ListenAddress, DnsmasqOptionKind.String },
|
||||
{ DnsmasqConfKeys.Port, DnsmasqOptionKind.String },
|
||||
{ DnsmasqConfKeys.User, DnsmasqOptionKind.String },
|
||||
{ DnsmasqConfKeys.Group, DnsmasqOptionKind.String },
|
||||
{ DnsmasqConfKeys.CacheSize, DnsmasqOptionKind.String },
|
||||
{ DnsmasqConfKeys.LocalTtl, DnsmasqOptionKind.String },
|
||||
{ DnsmasqConfKeys.LogFacility, DnsmasqOptionKind.String },
|
||||
{ "dhcp-ignore", DnsmasqOptionKind.String },
|
||||
{ "min-port", DnsmasqOptionKind.String },
|
||||
{ "max-port", DnsmasqOptionKind.String },
|
||||
{ "query-port", DnsmasqOptionKind.String },
|
||||
{ "edns-packet-max", DnsmasqOptionKind.String },
|
||||
// DHCP structured
|
||||
{ "dhcp-range", DnsmasqOptionKind.DhcpRange },
|
||||
{ "dhcp-host", DnsmasqOptionKind.DhcpHost },
|
||||
{ "dhcp-option", DnsmasqOptionKind.DhcpOption },
|
||||
{ DnsmasqConfKeys.DhcpRange, DnsmasqOptionKind.DhcpRange },
|
||||
{ DnsmasqConfKeys.DhcpHost, DnsmasqOptionKind.DhcpHost },
|
||||
{ DnsmasqConfKeys.DhcpOption, DnsmasqOptionKind.DhcpOption },
|
||||
// DNS structured
|
||||
{ "server", DnsmasqOptionKind.Server },
|
||||
{ "local", DnsmasqOptionKind.Local },
|
||||
{ "address", DnsmasqOptionKind.Address },
|
||||
{ DnsmasqConfKeys.Server, DnsmasqOptionKind.Server },
|
||||
{ DnsmasqConfKeys.Local, DnsmasqOptionKind.Local },
|
||||
{ DnsmasqConfKeys.Address, DnsmasqOptionKind.Address },
|
||||
};
|
||||
|
||||
/// <summary>Well-known flag options (no value).</summary>
|
||||
public static IReadOnlySet<string> FlagOptions { get; } =
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"domain-needed", "bogus-priv", "no-hosts", "expand-hosts", "strict-order",
|
||||
"no-resolv", "no-poll", "bind-interfaces", "no-negcache", "log-queries", "log-dhcp",
|
||||
"all-servers", "leasefile-ro", "dhcp-authoritative",
|
||||
DnsmasqConfKeys.DomainNeeded, DnsmasqConfKeys.BogusPriv, DnsmasqConfKeys.NoHosts, DnsmasqConfKeys.ExpandHosts, DnsmasqConfKeys.StrictOrder,
|
||||
DnsmasqConfKeys.NoResolv, DnsmasqConfKeys.NoPoll, DnsmasqConfKeys.BindInterfaces, DnsmasqConfKeys.NoNegcache, "log-queries", "log-dhcp",
|
||||
"all-servers", DnsmasqConfKeys.LeasefileRo, DnsmasqConfKeys.DhcpAuthoritative,
|
||||
"quiet-dhcp", "quiet-dhcp6", "quiet-ra", "dhcp-broadcast", "dhcp-sequential-ip",
|
||||
"enable-tftp", "self-resolve", "conntrack",
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using Superpower;
|
||||
using Superpower.Model;
|
||||
@@ -16,6 +17,10 @@ public static class DnsmasqConfIncludeParser
|
||||
{
|
||||
private static readonly StringComparison KeyComparison = StringComparison.Ordinal;
|
||||
|
||||
/// <summary>Gets lines for a path from pre-read content; used by config set cache to avoid double read.</summary>
|
||||
private static string[] GetLines(string path, IReadOnlyDictionary<string, string[]> pathToLines) =>
|
||||
pathToLines.TryGetValue(path, out var L) ? L : Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ordered list of absolute config file paths dnsmasq loads, in the exact order
|
||||
/// dnsmasq reads them (main interleaved with conf-file and conf-dir).
|
||||
@@ -80,7 +85,7 @@ public static class DnsmasqConfIncludeParser
|
||||
continue;
|
||||
var (key, value) = kv.Value;
|
||||
|
||||
if (string.Equals(key, "conf-file", KeyComparison))
|
||||
if (string.Equals(key, DnsmasqConfKeys.ConfFile, KeyComparison))
|
||||
{
|
||||
var path = value.Trim();
|
||||
if (string.IsNullOrEmpty(path)) continue;
|
||||
@@ -90,7 +95,7 @@ public static class DnsmasqConfIncludeParser
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(key, "conf-dir", KeyComparison))
|
||||
if (string.Equals(key, DnsmasqConfKeys.ConfDir, KeyComparison))
|
||||
{
|
||||
var (directory, matchSuffix, ignoreSuffix) = ParseConfDirValue(value);
|
||||
if (string.IsNullOrEmpty(directory)) continue;
|
||||
@@ -192,7 +197,7 @@ public static class DnsmasqConfIncludeParser
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (key, value) = kv.Value;
|
||||
if (!string.Equals(key, "conf-dir", KeyComparison))
|
||||
if (!string.Equals(key, DnsmasqConfKeys.ConfDir, KeyComparison))
|
||||
continue;
|
||||
var path = value.Split(',')[0].Trim();
|
||||
return Path.GetFullPath(Path.Combine(mainDir, path));
|
||||
@@ -231,6 +236,30 @@ public static class DnsmasqConfIncludeParser
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetFlagFromConfigFiles(IReadOnlyList{string}, string)"/> but uses pre-read content (e.g. from config set cache).</summary>
|
||||
public static bool GetFlagFromConfigFiles(IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines, string optionKey)
|
||||
{
|
||||
var key = optionKey.Trim();
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return false;
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (k, v) = kv.Value;
|
||||
if (!string.Equals(k, key, KeyComparison))
|
||||
continue;
|
||||
if (!string.IsNullOrEmpty(v?.Trim()))
|
||||
continue;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last value for the given option key across config files, and the directory of the file
|
||||
/// that contained it (for resolving relative paths). Keys matched case-sensitively per dnsmasq.
|
||||
@@ -263,6 +292,32 @@ public static class DnsmasqConfIncludeParser
|
||||
return (lastValue, lastDir);
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetLastValueFromConfigFiles(IReadOnlyList{string}, string)"/> but uses pre-read content.</summary>
|
||||
public static (string? Value, string? ConfigFileDir) GetLastValueFromConfigFiles(IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines, string optionKey)
|
||||
{
|
||||
var key = optionKey.Trim();
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return (null, null);
|
||||
string? lastValue = null;
|
||||
string? lastDir = null;
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(configPath) ?? "";
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (k, value) = kv.Value;
|
||||
if (!string.Equals(k, key, KeyComparison))
|
||||
continue;
|
||||
lastValue = value.Trim();
|
||||
lastDir = dir;
|
||||
}
|
||||
}
|
||||
return (lastValue, lastDir);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a path value against the config file directory. If value is null/empty or already absolute, returns as-is (or null).
|
||||
/// </summary>
|
||||
@@ -295,7 +350,30 @@ public static class DnsmasqConfIncludeParser
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (key, value) = kv.Value;
|
||||
if (!string.Equals(key, "dhcp-leasefile", KeyComparison) && !string.Equals(key, "dhcp-lease", KeyComparison))
|
||||
if (!string.Equals(key, DnsmasqConfKeys.DhcpLeasefile, KeyComparison) && !string.Equals(key, DnsmasqConfKeys.DhcpLease, KeyComparison))
|
||||
continue;
|
||||
var path = value.Trim();
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
result = ResolvePath(path, dir) ?? result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetDhcpLeaseFilePathFromConfigFiles(IReadOnlyList{string})"/> but uses pre-read content.</summary>
|
||||
public static string? GetDhcpLeaseFilePathFromConfigFiles(IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines)
|
||||
{
|
||||
string? result = null;
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(configPath) ?? "";
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (key, value) = kv.Value;
|
||||
if (!string.Equals(key, DnsmasqConfKeys.DhcpLeasefile, KeyComparison) && !string.Equals(key, DnsmasqConfKeys.DhcpLease, KeyComparison))
|
||||
continue;
|
||||
var path = value.Trim();
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
@@ -310,7 +388,11 @@ public static class DnsmasqConfIncludeParser
|
||||
/// When true, dnsmasq does not read /etc/hosts; only addn-hosts= files are used (if any).
|
||||
/// </summary>
|
||||
public static bool GetNoHostsFromConfigFiles(IReadOnlyList<string> configFilePathsInOrder) =>
|
||||
GetFlagFromConfigFiles(configFilePathsInOrder, "no-hosts");
|
||||
GetFlagFromConfigFiles(configFilePathsInOrder, DnsmasqConfKeys.NoHosts);
|
||||
|
||||
/// <summary>Same as <see cref="GetNoHostsFromConfigFiles(IReadOnlyList{string})"/> but uses pre-read content.</summary>
|
||||
public static bool GetNoHostsFromConfigFiles(IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines) =>
|
||||
GetFlagFromConfigFiles(configFilePathsInOrder, pathToLines, DnsmasqConfKeys.NoHosts);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the given config files in order and returns all <c>addn-hosts=</c> paths (cumulative; dnsmasq loads each in order).
|
||||
@@ -330,7 +412,31 @@ public static class DnsmasqConfIncludeParser
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (key, value) = kv.Value;
|
||||
if (!string.Equals(key, "addn-hosts", KeyComparison))
|
||||
if (!string.Equals(key, DnsmasqConfKeys.AddnHosts, KeyComparison))
|
||||
continue;
|
||||
var path = value.Trim();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
continue;
|
||||
result.Add(Path.GetFullPath(Path.Combine(dir, path)));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetAddnHostsPathsFromConfigFiles(IReadOnlyList{string})"/> but uses pre-read content.</summary>
|
||||
public static IReadOnlyList<string> GetAddnHostsPathsFromConfigFiles(IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines)
|
||||
{
|
||||
var result = new List<string>();
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(configPath) ?? "";
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (key, value) = kv.Value;
|
||||
if (!string.Equals(key, DnsmasqConfKeys.AddnHosts, KeyComparison))
|
||||
continue;
|
||||
var path = value.Trim();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
@@ -374,6 +480,32 @@ public static class DnsmasqConfIncludeParser
|
||||
return (lastValue, lastSource);
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetLastValueFromConfigFilesWithSource(IReadOnlyList{string}, string, string?)"/> but uses pre-read content.</summary>
|
||||
public static (string? Value, ConfigValueSource? Source) GetLastValueFromConfigFilesWithSource(
|
||||
IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines, string optionKey, string? managedFilePath)
|
||||
{
|
||||
var key = optionKey.Trim();
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return (null, null);
|
||||
string? lastValue = null;
|
||||
ConfigValueSource? lastSource = null;
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (k, value) = kv.Value;
|
||||
if (!string.Equals(k, key, KeyComparison))
|
||||
continue;
|
||||
lastValue = value.Trim();
|
||||
lastSource = MakeSource(configPath, managedFilePath);
|
||||
}
|
||||
}
|
||||
return (lastValue, lastSource);
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="GetFlagFromConfigFiles"/> but returns which file set the flag (for readonly: if not managed, user cannot unset from UI).</summary>
|
||||
public static (bool IsSet, ConfigValueSource? Source) GetFlagFromConfigFilesWithSource(
|
||||
IReadOnlyList<string> configFilePathsInOrder, string optionKey, string? managedFilePath)
|
||||
@@ -401,6 +533,31 @@ public static class DnsmasqConfIncludeParser
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetFlagFromConfigFilesWithSource(IReadOnlyList{string}, string, string?)"/> but uses pre-read content.</summary>
|
||||
public static (bool IsSet, ConfigValueSource? Source) GetFlagFromConfigFilesWithSource(
|
||||
IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines, string optionKey, string? managedFilePath)
|
||||
{
|
||||
var key = optionKey.Trim();
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return (false, null);
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (k, v) = kv.Value;
|
||||
if (!string.Equals(k, key, KeyComparison))
|
||||
continue;
|
||||
if (!string.IsNullOrEmpty(v?.Trim()))
|
||||
continue;
|
||||
return (true, MakeSource(configPath, managedFilePath));
|
||||
}
|
||||
}
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="GetDhcpLeaseFilePathFromConfigFiles"/> but returns which file set the value.</summary>
|
||||
public static (string? Path, ConfigValueSource? Source) GetDhcpLeaseFilePathFromConfigFilesWithSource(
|
||||
IReadOnlyList<string> configFilePathsInOrder, string? managedFilePath)
|
||||
@@ -418,7 +575,35 @@ public static class DnsmasqConfIncludeParser
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (key, value) = kv.Value;
|
||||
if (!string.Equals(key, "dhcp-leasefile", KeyComparison) && !string.Equals(key, "dhcp-lease", KeyComparison))
|
||||
if (!string.Equals(key, DnsmasqConfKeys.DhcpLeasefile, KeyComparison) && !string.Equals(key, DnsmasqConfKeys.DhcpLease, KeyComparison))
|
||||
continue;
|
||||
var path = value.Trim();
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
result = ResolvePath(path, dir) ?? result;
|
||||
lastSource = MakeSource(configPath, managedFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (result, lastSource);
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetDhcpLeaseFilePathFromConfigFilesWithSource(IReadOnlyList{string}, string?)"/> but uses pre-read content.</summary>
|
||||
public static (string? Path, ConfigValueSource? Source) GetDhcpLeaseFilePathFromConfigFilesWithSource(
|
||||
IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines, string? managedFilePath)
|
||||
{
|
||||
string? result = null;
|
||||
ConfigValueSource? lastSource = null;
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(configPath) ?? "";
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (key, value) = kv.Value;
|
||||
if (!string.Equals(key, DnsmasqConfKeys.DhcpLeasefile, KeyComparison) && !string.Equals(key, DnsmasqConfKeys.DhcpLease, KeyComparison))
|
||||
continue;
|
||||
var path = value.Trim();
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
@@ -448,7 +633,33 @@ public static class DnsmasqConfIncludeParser
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (key, value) = kv.Value;
|
||||
if (!string.Equals(key, "addn-hosts", KeyComparison))
|
||||
if (!string.Equals(key, DnsmasqConfKeys.AddnHosts, KeyComparison))
|
||||
continue;
|
||||
var path = value.Trim();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
continue;
|
||||
result.Add((Path.GetFullPath(Path.Combine(dir, path)), source));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetAddnHostsPathsFromConfigFilesWithSource(IReadOnlyList{string}, string?)"/> but uses pre-read content.</summary>
|
||||
public static IReadOnlyList<(string Path, ConfigValueSource Source)> GetAddnHostsPathsFromConfigFilesWithSource(
|
||||
IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines, string? managedFilePath)
|
||||
{
|
||||
var result = new List<(string Path, ConfigValueSource Source)>();
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(configPath) ?? "";
|
||||
var source = MakeSource(configPath, managedFilePath);
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (key, value) = kv.Value;
|
||||
if (!string.Equals(key, DnsmasqConfKeys.AddnHosts, KeyComparison))
|
||||
continue;
|
||||
var path = value.Trim();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
@@ -485,6 +696,30 @@ public static class DnsmasqConfIncludeParser
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetMultiValueFromConfigFiles(IReadOnlyList{string}, string)"/> but uses pre-read content.</summary>
|
||||
public static IReadOnlyList<string> GetMultiValueFromConfigFiles(
|
||||
IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines, string optionKey)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var key = optionKey.Trim();
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return result;
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (k, value) = kv.Value;
|
||||
if (!string.Equals(k, key, KeyComparison))
|
||||
continue;
|
||||
result.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Collects all values for multiple option keys (e.g. server and local) in file order. Keys matched case-sensitively.</summary>
|
||||
public static IReadOnlyList<string> GetMultiValueFromConfigFiles(
|
||||
IReadOnlyList<string> configFilePathsInOrder, IReadOnlyList<string> optionKeys)
|
||||
@@ -511,6 +746,30 @@ public static class DnsmasqConfIncludeParser
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetMultiValueFromConfigFiles(IReadOnlyList{string}, IReadOnlyList{string})"/> but uses pre-read content.</summary>
|
||||
public static IReadOnlyList<string> GetMultiValueFromConfigFiles(
|
||||
IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines, IReadOnlyList<string> optionKeys)
|
||||
{
|
||||
var keys = new HashSet<string>(optionKeys.Select(k => k.Trim()).Where(k => k.Length > 0), StringComparer.Ordinal);
|
||||
if (keys.Count == 0)
|
||||
return Array.Empty<string>();
|
||||
var result = new List<string>();
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (k, value) = kv.Value;
|
||||
if (!keys.Contains(k))
|
||||
continue;
|
||||
result.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="GetMultiValueFromConfigFiles(IReadOnlyList{string}, string)"/> but returns source per value.</summary>
|
||||
public static IReadOnlyList<(string Value, ConfigValueSource Source)> GetMultiValueFromConfigFilesWithSource(
|
||||
IReadOnlyList<string> configFilePathsInOrder, string optionKey, string? managedFilePath)
|
||||
@@ -538,6 +797,31 @@ public static class DnsmasqConfIncludeParser
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetMultiValueFromConfigFilesWithSource(IReadOnlyList{string}, string, string?)"/> but uses pre-read content.</summary>
|
||||
public static IReadOnlyList<(string Value, ConfigValueSource Source)> GetMultiValueFromConfigFilesWithSource(
|
||||
IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines, string optionKey, string? managedFilePath)
|
||||
{
|
||||
var result = new List<(string Value, ConfigValueSource Source)>();
|
||||
var key = optionKey.Trim();
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return result;
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
var source = MakeSource(configPath, managedFilePath);
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (k, value) = kv.Value;
|
||||
if (!string.Equals(k, key, KeyComparison))
|
||||
continue;
|
||||
result.Add((value.Trim(), source));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="GetMultiValueFromConfigFiles(IReadOnlyList{string}, IReadOnlyList{string})"/> but returns source per value.</summary>
|
||||
public static IReadOnlyList<(string Value, ConfigValueSource Source)> GetMultiValueFromConfigFilesWithSource(
|
||||
IReadOnlyList<string> configFilePathsInOrder, IReadOnlyList<string> optionKeys, string? managedFilePath)
|
||||
@@ -564,4 +848,29 @@ public static class DnsmasqConfIncludeParser
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Same as <see cref="GetMultiValueFromConfigFilesWithSource(IReadOnlyList{string}, IReadOnlyList{string}, string?)"/> but uses pre-read content.</summary>
|
||||
public static IReadOnlyList<(string Value, ConfigValueSource Source)> GetMultiValueFromConfigFilesWithSource(
|
||||
IReadOnlyList<string> configFilePathsInOrder, IReadOnlyDictionary<string, string[]> pathToLines, IReadOnlyList<string> optionKeys, string? managedFilePath)
|
||||
{
|
||||
var keys = new HashSet<string>(optionKeys.Select(k => k.Trim()).Where(k => k.Length > 0), StringComparer.Ordinal);
|
||||
if (keys.Count == 0)
|
||||
return Array.Empty<(string, ConfigValueSource)>();
|
||||
var result = new List<(string Value, ConfigValueSource Source)>();
|
||||
foreach (var configPath in configFilePathsInOrder)
|
||||
{
|
||||
var source = MakeSource(configPath, managedFilePath);
|
||||
foreach (var line in GetLines(configPath, pathToLines))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
continue;
|
||||
var (k, value) = kv.Value;
|
||||
if (!keys.Contains(k))
|
||||
continue;
|
||||
result.Add((value.Trim(), source));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
|
||||
/// <summary>Immutable snapshot from the config set cache: config set, effective config, sources, managed file content, and DHCP host entries (one read per refresh).</summary>
|
||||
public record ConfigSetSnapshot(
|
||||
DnsmasqConfigSet Set,
|
||||
EffectiveDnsmasqConfig Config,
|
||||
EffectiveConfigSources Sources,
|
||||
ManagedConfigContent ManagedContent,
|
||||
IReadOnlyList<DhcpHostEntry> DhcpHostEntries
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
|
||||
/// <summary>Immutable snapshot from the hosts cache: managed hosts file entries and read-only hosts files (system + addn-hosts).</summary>
|
||||
public record HostsSnapshot(
|
||||
IReadOnlyList<HostEntry> ManagedEntries,
|
||||
IReadOnlyList<ReadOnlyHostsFile> ReadOnlyFiles
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton cache for the config set (main + conf-dir + managed file). Invalidates on file watchers, staleness, or manual Invalidate.
|
||||
/// After the app writes the managed config file, call <see cref="NotifyWeWroteManagedConfig"/> so the cache updates in place instead of treating the write as an external change.
|
||||
/// </summary>
|
||||
public interface IConfigSetCache : IApplicationSingleton
|
||||
{
|
||||
/// <summary>Returns the current snapshot (config set, effective config, sources, managed content), refreshing from disk if dirty or stale.</summary>
|
||||
Task<ConfigSetSnapshot> GetSnapshotAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Forces the next <see cref="GetSnapshotAsync"/> to re-read all config files.</summary>
|
||||
void Invalidate();
|
||||
|
||||
/// <summary>Call after the app writes the managed config file. Updates the cached managed content in place and ignores the next watcher event for that file for a short window.</summary>
|
||||
void NotifyWeWroteManagedConfig(ManagedConfigContent newContent);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton cache for hosts: managed hosts file + read-only (system + addn-hosts). Invalidates on watchers, staleness, or Invalidate.
|
||||
/// Call <see cref="NotifyWeWroteManagedHosts"/> after the app writes the managed hosts file so the cache updates in place and ignores the next watcher event.
|
||||
/// </summary>
|
||||
public interface IHostsCache : IApplicationSingleton
|
||||
{
|
||||
Task<HostsSnapshot> GetSnapshotAsync(CancellationToken ct = default);
|
||||
void Invalidate();
|
||||
void NotifyWeWroteManagedHosts(IReadOnlyList<HostEntry> entries);
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
using System.Text;
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton cache for the config set (main + conf-dir + managed file). Reads all files once per refresh,
|
||||
/// builds snapshot from pathToLines via parser overloads. Invalidates on watchers (main + managed file), staleness, or Invalidate.
|
||||
/// Self-write: NotifyWeWroteManagedConfig updates cached managed content in place and ignores the next managed-file watcher event for a short window.
|
||||
/// </summary>
|
||||
public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
||||
{
|
||||
private const int StaleCacheSeconds = 120;
|
||||
private const double SelfWriteIgnoreSeconds = 1.5;
|
||||
|
||||
private readonly DnsmasqOptions _options;
|
||||
private readonly ILogger<ConfigSetCache> _logger;
|
||||
private readonly object _lock = new();
|
||||
private ConfigSetSnapshot? _snapshot;
|
||||
private DateTime? _lastReadUtc;
|
||||
private bool _dirty = true;
|
||||
private DateTime _lastWriteManagedUtc = DateTime.MinValue;
|
||||
private FileSystemWatcher? _watcherMain;
|
||||
private FileSystemWatcher? _watcherManaged;
|
||||
|
||||
public ConfigSetCache(IOptions<DnsmasqOptions> options, ILogger<ConfigSetCache> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
var (mainPath, managedPath) = GetPaths();
|
||||
if (!string.IsNullOrEmpty(mainPath))
|
||||
TryAddWatcher(Path.GetDirectoryName(mainPath), Path.GetFileName(mainPath), ref _watcherMain, "main config");
|
||||
if (!string.IsNullOrEmpty(managedPath))
|
||||
TryAddWatcher(Path.GetDirectoryName(managedPath), Path.GetFileName(managedPath), ref _watcherManaged, "managed config");
|
||||
}
|
||||
|
||||
private (string? MainPath, string? ManagedPath) GetPaths()
|
||||
{
|
||||
var mainPath = _options.MainConfigPath;
|
||||
if (string.IsNullOrEmpty(mainPath))
|
||||
return (null, null);
|
||||
var mainFull = Path.GetFullPath(mainPath);
|
||||
var mainDir = Path.GetDirectoryName(mainFull) ?? "";
|
||||
var managedPath = Path.Combine(mainDir, _options.ManagedFileName);
|
||||
return (mainFull, managedPath);
|
||||
}
|
||||
|
||||
private void TryAddWatcher(string? dir, string? fileName, ref FileSystemWatcher? watcher, string label)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dir) || string.IsNullOrEmpty(fileName))
|
||||
return;
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
_logger.LogDebug("{Label} directory does not exist yet: {Dir}", label, dir);
|
||||
return;
|
||||
}
|
||||
watcher = new FileSystemWatcher(dir)
|
||||
{
|
||||
Filter = fileName,
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size
|
||||
};
|
||||
watcher.Changed += OnFileChanged;
|
||||
watcher.Created += OnFileChanged;
|
||||
watcher.EnableRaisingEvents = true;
|
||||
_logger.LogDebug("Watching {Label}: {Path}", label, Path.Combine(dir, fileName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not create file watcher for {Label}: {Dir}", label, dir);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFileChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(e.FullPath);
|
||||
var (_, managedPath) = GetPaths();
|
||||
if (!string.IsNullOrEmpty(managedPath) && string.Equals(fullPath, Path.GetFullPath(managedPath), StringComparison.Ordinal))
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var elapsed = (DateTime.UtcNow - _lastWriteManagedUtc).TotalSeconds;
|
||||
if (elapsed < SelfWriteIgnoreSeconds)
|
||||
return;
|
||||
}
|
||||
}
|
||||
lock (_lock)
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
public void Invalidate()
|
||||
{
|
||||
lock (_lock)
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
public void NotifyWeWroteManagedConfig(ManagedConfigContent newContent)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_lastWriteManagedUtc = DateTime.UtcNow;
|
||||
_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ConfigSetSnapshot> GetSnapshotAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await Task.Run(() => GetSnapshot(ct), ct);
|
||||
}
|
||||
|
||||
private ConfigSetSnapshot GetSnapshot(CancellationToken ct)
|
||||
{
|
||||
var (mainFull, managedPath) = GetPaths();
|
||||
if (string.IsNullOrEmpty(mainFull))
|
||||
return CreateDefaultSnapshot();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_dirty && _snapshot != null && _lastReadUtc.HasValue)
|
||||
{
|
||||
var ageSeconds = (DateTime.UtcNow - _lastReadUtc.Value).TotalSeconds;
|
||||
if (ageSeconds < StaleCacheSeconds)
|
||||
return _snapshot;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
var managedHostsFileName = (_options.ManagedHostsFileName ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(managedHostsFileName))
|
||||
managedHostsFileName = "zz-dnsmasq-webui.hosts";
|
||||
var mainDir = Path.GetDirectoryName(mainFull) ?? "";
|
||||
var managedHostsFilePath = Path.Combine(mainDir, managedHostsFileName);
|
||||
var set = BuildConfigSet(mainFull, managedPath, managedHostsFilePath);
|
||||
if (set.Files.Count == 0)
|
||||
{
|
||||
_snapshot = CreateDefaultSnapshot();
|
||||
_lastReadUtc = DateTime.UtcNow;
|
||||
_dirty = false;
|
||||
return _snapshot;
|
||||
}
|
||||
|
||||
var paths = set.Files.Select(f => f.Path).ToList();
|
||||
var pathToLines = ReadAllPaths(paths, ct);
|
||||
var config = BuildEffectiveConfig(paths, pathToLines);
|
||||
var sources = BuildEffectiveConfigSources(paths, pathToLines, set.ManagedFilePath);
|
||||
var managedContent = BuildManagedContent(pathToLines, set.ManagedFilePath);
|
||||
var dhcpHostEntries = BuildDhcpHostEntries(set, pathToLines);
|
||||
|
||||
_snapshot = new ConfigSetSnapshot(set, config, sources, managedContent, dhcpHostEntries);
|
||||
_lastReadUtc = DateTime.UtcNow;
|
||||
_dirty = false;
|
||||
return _snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
private static ConfigSetSnapshot CreateDefaultSnapshot()
|
||||
{
|
||||
var set = new DnsmasqConfigSet("", "", null, Array.Empty<DnsmasqConfigSetEntry>());
|
||||
var config = CreateDefaultEffectiveConfig();
|
||||
var sources = CreateDefaultEffectiveConfigSources();
|
||||
var managedContent = new ManagedConfigContent(Array.Empty<DnsmasqConfLine>(), "");
|
||||
return new ConfigSetSnapshot(set, config, sources, managedContent, Array.Empty<DhcpHostEntry>());
|
||||
}
|
||||
|
||||
private static DnsmasqConfigSet BuildConfigSet(string mainFull, string? managedFilePath, string managedHostsFilePath)
|
||||
{
|
||||
var mainPath = mainFull;
|
||||
var mainDir = Path.GetDirectoryName(mainFull) ?? "";
|
||||
|
||||
var withSource = DnsmasqConfIncludeParser.GetIncludedPathsWithSource(mainPath);
|
||||
var files = withSource.Select(p => new DnsmasqConfigSetEntry(
|
||||
p.Path,
|
||||
Path.GetFileName(p.Path),
|
||||
p.Source,
|
||||
IsManaged: string.Equals(p.Path, managedFilePath, StringComparison.Ordinal)
|
||||
)).ToList();
|
||||
|
||||
if (managedFilePath != null && files.All(e => !string.Equals(e.Path, managedFilePath, StringComparison.Ordinal)))
|
||||
files.Add(new DnsmasqConfigSetEntry(managedFilePath, Path.GetFileName(managedFilePath), DnsmasqConfFileSource.ConfFile, IsManaged: true));
|
||||
|
||||
return new DnsmasqConfigSet(mainFull, managedFilePath ?? "", managedHostsFilePath, files);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string[]> ReadAllPaths(IReadOnlyList<string> paths, CancellationToken ct)
|
||||
{
|
||||
var dict = new Dictionary<string, string[]>(StringComparer.Ordinal);
|
||||
foreach (var path in paths)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var canonical = Path.GetFullPath(path);
|
||||
if (dict.ContainsKey(canonical))
|
||||
continue;
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
dict[canonical] = Array.Empty<string>();
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
dict[canonical] = File.ReadAllLines(path, Encoding.UTF8);
|
||||
}
|
||||
catch
|
||||
{
|
||||
dict[canonical] = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
private static EffectiveDnsmasqConfig BuildEffectiveConfig(IReadOnlyList<string> paths, IReadOnlyDictionary<string, string[]> pathToLines)
|
||||
{
|
||||
var noHosts = DnsmasqConfIncludeParser.GetNoHostsFromConfigFiles(paths, pathToLines);
|
||||
var addnHosts = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFiles(paths, pathToLines);
|
||||
var serverLocal = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.ServerLocalKeys);
|
||||
var addressValues = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.Address);
|
||||
var interfaces = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.Interface);
|
||||
var listenAddresses = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.ListenAddress);
|
||||
var exceptInterfaces = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.ExceptInterface);
|
||||
var dhcpRanges = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpRange);
|
||||
var dhcpHostLines = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpHost);
|
||||
var dhcpOptionLines = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpOption);
|
||||
var resolvFiles = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.ResolvFile);
|
||||
|
||||
var expandHosts = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.ExpandHosts);
|
||||
var bogusPriv = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.BogusPriv);
|
||||
var strictOrder = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.StrictOrder);
|
||||
var noResolv = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.NoResolv);
|
||||
var domainNeeded = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DomainNeeded);
|
||||
var noPoll = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.NoPoll);
|
||||
var bindInterfaces = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.BindInterfaces);
|
||||
var noNegcache = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.NoNegcache);
|
||||
var dhcpAuthoritative = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpAuthoritative);
|
||||
var leasefileRo = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.LeasefileRo);
|
||||
|
||||
var dhcpLeaseFilePath = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(paths, pathToLines);
|
||||
|
||||
var (cacheVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.CacheSize);
|
||||
var cacheSize = TryParseInt(cacheVal);
|
||||
var (portVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.Port);
|
||||
var port = TryParseInt(portVal);
|
||||
var (localTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.LocalTtl);
|
||||
var localTtl = TryParseInt(localTtlVal);
|
||||
var (pidVal, pidDir) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.PidFile);
|
||||
var pidFilePath = DnsmasqConfIncludeParser.ResolvePath(pidVal, pidDir);
|
||||
var (userVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.User);
|
||||
var user = string.IsNullOrWhiteSpace(userVal) ? null : userVal.Trim();
|
||||
var (groupVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.Group);
|
||||
var group = string.IsNullOrWhiteSpace(groupVal) ? null : groupVal.Trim();
|
||||
var (logFacVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.LogFacility);
|
||||
var logFacility = string.IsNullOrWhiteSpace(logFacVal) ? null : logFacVal.Trim();
|
||||
var (leaseMaxVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpLeaseMax);
|
||||
var dhcpLeaseMax = TryParseInt(leaseMaxVal);
|
||||
var (negTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.NegTtl);
|
||||
var negTtl = TryParseInt(negTtlVal);
|
||||
var (maxTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.MaxTtl);
|
||||
var maxTtl = TryParseInt(maxTtlVal);
|
||||
var (maxCacheTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.MaxCacheTtl);
|
||||
var maxCacheTtl = TryParseInt(maxCacheTtlVal);
|
||||
var (minCacheTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.MinCacheTtl);
|
||||
var minCacheTtl = TryParseInt(minCacheTtlVal);
|
||||
var (dhcpTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpTtl);
|
||||
var dhcpTtl = TryParseInt(dhcpTtlVal);
|
||||
|
||||
return new EffectiveDnsmasqConfig(
|
||||
noHosts, addnHosts,
|
||||
serverLocal, addressValues, interfaces, listenAddresses, exceptInterfaces, dhcpRanges, dhcpHostLines, dhcpOptionLines, resolvFiles,
|
||||
expandHosts, bogusPriv, strictOrder, noResolv, domainNeeded, noPoll, bindInterfaces, noNegcache, dhcpAuthoritative, leasefileRo,
|
||||
dhcpLeaseFilePath, cacheSize, port, localTtl, pidFilePath, user, group, logFacility, dhcpLeaseMax,
|
||||
negTtl, maxTtl, maxCacheTtl, minCacheTtl, dhcpTtl
|
||||
);
|
||||
}
|
||||
|
||||
private static EffectiveConfigSources BuildEffectiveConfigSources(IReadOnlyList<string> paths, IReadOnlyDictionary<string, string[]> pathToLines, string? managedFilePath)
|
||||
{
|
||||
var (_, noHostsSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.NoHosts, managedFilePath);
|
||||
var addnHostsWithSource = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFilesWithSource(paths, pathToLines, managedFilePath);
|
||||
var serverLocalWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.ServerLocalKeys, managedFilePath);
|
||||
var addressWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.Address, managedFilePath);
|
||||
var interfacesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.Interface, managedFilePath);
|
||||
var listenAddressesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.ListenAddress, managedFilePath);
|
||||
var exceptInterfacesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.ExceptInterface, managedFilePath);
|
||||
var dhcpRangesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpRange, managedFilePath);
|
||||
var dhcpHostLinesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpHost, managedFilePath);
|
||||
var dhcpOptionLinesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpOption, managedFilePath);
|
||||
var resolvFilesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.ResolvFile, managedFilePath);
|
||||
|
||||
var (_, expandHostsSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.ExpandHosts, managedFilePath);
|
||||
var (_, bogusPrivSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.BogusPriv, managedFilePath);
|
||||
var (_, strictOrderSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.StrictOrder, managedFilePath);
|
||||
var (_, noResolvSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.NoResolv, managedFilePath);
|
||||
var (_, domainNeededSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DomainNeeded, managedFilePath);
|
||||
var (_, noPollSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.NoPoll, managedFilePath);
|
||||
var (_, bindInterfacesSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.BindInterfaces, managedFilePath);
|
||||
var (_, noNegcacheSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.NoNegcache, managedFilePath);
|
||||
var (_, dhcpAuthoritativeSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpAuthoritative, managedFilePath);
|
||||
var (_, leasefileRoSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.LeasefileRo, managedFilePath);
|
||||
|
||||
var (_, dhcpLeaseFilePathSource) = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFilesWithSource(paths, pathToLines, managedFilePath);
|
||||
var (_, cacheSizeSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.CacheSize, managedFilePath);
|
||||
var (_, portSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.Port, managedFilePath);
|
||||
var (_, localTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.LocalTtl, managedFilePath);
|
||||
var (_, pidFilePathSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.PidFile, managedFilePath);
|
||||
var (_, userSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.User, managedFilePath);
|
||||
var (_, groupSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.Group, managedFilePath);
|
||||
var (_, logFacilitySource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.LogFacility, managedFilePath);
|
||||
var (_, dhcpLeaseMaxSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpLeaseMax, managedFilePath);
|
||||
var (_, negTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.NegTtl, managedFilePath);
|
||||
var (_, maxTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.MaxTtl, managedFilePath);
|
||||
var (_, maxCacheTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.MaxCacheTtl, managedFilePath);
|
||||
var (_, minCacheTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.MinCacheTtl, managedFilePath);
|
||||
var (_, dhcpTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpTtl, managedFilePath);
|
||||
|
||||
return new EffectiveConfigSources(
|
||||
noHostsSource, addnHostsWithSource.Select(t => new PathWithSource(t.Path, t.Source)).ToList(),
|
||||
serverLocalWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
addressWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
interfacesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
listenAddressesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
exceptInterfacesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
dhcpRangesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
dhcpHostLinesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
dhcpOptionLinesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
resolvFilesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
expandHostsSource, bogusPrivSource, strictOrderSource, noResolvSource, domainNeededSource, noPollSource,
|
||||
bindInterfacesSource, noNegcacheSource, dhcpAuthoritativeSource, leasefileRoSource,
|
||||
dhcpLeaseFilePathSource, cacheSizeSource, portSource, localTtlSource, pidFilePathSource, userSource, groupSource,
|
||||
logFacilitySource, dhcpLeaseMaxSource, negTtlSource, maxTtlSource, maxCacheTtlSource, minCacheTtlSource, dhcpTtlSource
|
||||
);
|
||||
}
|
||||
|
||||
private static ManagedConfigContent BuildManagedContent(IReadOnlyDictionary<string, string[]> pathToLines, string? managedFilePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(managedFilePath))
|
||||
return new ManagedConfigContent(Array.Empty<DnsmasqConfLine>(), "");
|
||||
var canonical = Path.GetFullPath(managedFilePath);
|
||||
if (!pathToLines.TryGetValue(canonical, out var lines))
|
||||
return new ManagedConfigContent(Array.Empty<DnsmasqConfLine>(), "");
|
||||
var configLines = DnsmasqConfFileLineParser.ParseFile(lines);
|
||||
var effectiveHostsPath = configLines.OfType<AddnHostsLine>().FirstOrDefault()?.AddnHostsPath ?? "";
|
||||
return new ManagedConfigContent(configLines, effectiveHostsPath);
|
||||
}
|
||||
|
||||
private static List<DhcpHostEntry> BuildDhcpHostEntries(DnsmasqConfigSet set, IReadOnlyDictionary<string, string[]> pathToLines)
|
||||
{
|
||||
var result = new List<DhcpHostEntry>();
|
||||
foreach (var file in set.Files)
|
||||
{
|
||||
var canonical = Path.GetFullPath(file.Path);
|
||||
if (!pathToLines.TryGetValue(canonical, out var lines))
|
||||
continue;
|
||||
var configLines = DnsmasqConfFileLineParser.ParseFile(lines);
|
||||
foreach (var dhcpLine in configLines.OfType<DhcpHostLine>())
|
||||
{
|
||||
var entry = dhcpLine.DhcpHost;
|
||||
entry.SourcePath = file.Path;
|
||||
entry.IsEditable = file.IsManaged;
|
||||
result.Add(entry);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static EffectiveDnsmasqConfig CreateDefaultEffectiveConfig() =>
|
||||
new(
|
||||
NoHosts: false, AddnHostsPaths: Array.Empty<string>(),
|
||||
ServerLocalValues: Array.Empty<string>(), AddressValues: Array.Empty<string>(), Interfaces: Array.Empty<string>(),
|
||||
ListenAddresses: Array.Empty<string>(), ExceptInterfaces: Array.Empty<string>(), DhcpRanges: Array.Empty<string>(),
|
||||
DhcpHostLines: Array.Empty<string>(), DhcpOptionLines: Array.Empty<string>(), ResolvFiles: Array.Empty<string>(),
|
||||
ExpandHosts: false, BogusPriv: false, StrictOrder: false, NoResolv: false, DomainNeeded: false, NoPoll: false,
|
||||
BindInterfaces: false, NoNegcache: false, DhcpAuthoritative: false, LeasefileRo: false,
|
||||
DhcpLeaseFilePath: null, CacheSize: null, Port: null, LocalTtl: null, PidFilePath: null, User: null, Group: null,
|
||||
LogFacility: null, DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null
|
||||
);
|
||||
|
||||
private static EffectiveConfigSources CreateDefaultEffectiveConfigSources() =>
|
||||
new(
|
||||
NoHosts: null, AddnHostsPaths: Array.Empty<PathWithSource>(),
|
||||
ServerLocalValues: Array.Empty<ValueWithSource>(), AddressValues: Array.Empty<ValueWithSource>(),
|
||||
Interfaces: Array.Empty<ValueWithSource>(), ListenAddresses: Array.Empty<ValueWithSource>(),
|
||||
ExceptInterfaces: Array.Empty<ValueWithSource>(), DhcpRanges: Array.Empty<ValueWithSource>(),
|
||||
DhcpHostLines: Array.Empty<ValueWithSource>(), DhcpOptionLines: Array.Empty<ValueWithSource>(),
|
||||
ResolvFiles: Array.Empty<ValueWithSource>(),
|
||||
ExpandHosts: null, BogusPriv: null, StrictOrder: null, NoResolv: null, DomainNeeded: null, NoPoll: null,
|
||||
BindInterfaces: null, NoNegcache: null, DhcpAuthoritative: null, LeasefileRo: null,
|
||||
DhcpLeaseFilePath: null, CacheSize: null, Port: null, LocalTtl: null, PidFilePath: null, User: null, Group: null,
|
||||
LogFacility: null, DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null
|
||||
);
|
||||
|
||||
private static int? TryParseInt(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
return int.TryParse(value.Trim(), out var n) ? n : null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_watcherMain?.Dispose();
|
||||
_watcherMain = null;
|
||||
_watcherManaged?.Dispose();
|
||||
_watcherManaged = null;
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,13 @@ namespace DnsmasqWebUI.Services;
|
||||
public class DnsmasqConfigService : IDnsmasqConfigService
|
||||
{
|
||||
private readonly IDnsmasqConfigSetService _configSetService;
|
||||
private readonly IConfigSetCache _configSetCache;
|
||||
private readonly ILogger<DnsmasqConfigService> _logger;
|
||||
|
||||
public DnsmasqConfigService(IDnsmasqConfigSetService configSetService, ILogger<DnsmasqConfigService> logger)
|
||||
public DnsmasqConfigService(IDnsmasqConfigSetService configSetService, IConfigSetCache configSetCache, ILogger<DnsmasqConfigService> logger)
|
||||
{
|
||||
_configSetService = configSetService;
|
||||
_configSetCache = configSetCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -26,27 +28,13 @@ public class DnsmasqConfigService : IDnsmasqConfigService
|
||||
|
||||
public async Task<IReadOnlyList<DhcpHostEntry>> ReadDhcpHostsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var set = await _configSetService.GetConfigSetAsync(ct);
|
||||
if (string.IsNullOrEmpty(set.ManagedFilePath))
|
||||
var snapshot = await _configSetCache.GetSnapshotAsync(ct);
|
||||
if (string.IsNullOrEmpty(snapshot.Set.ManagedFilePath))
|
||||
{
|
||||
_logger.LogDebug("No managed file path (no conf-dir in main config); returning empty dhcp hosts");
|
||||
return Array.Empty<DhcpHostEntry>();
|
||||
}
|
||||
var allEntries = new List<DhcpHostEntry>();
|
||||
foreach (var file in set.Files)
|
||||
{
|
||||
if (!File.Exists(file.Path))
|
||||
continue;
|
||||
var lines = await File.ReadAllLinesAsync(file.Path, Encoding.UTF8, ct);
|
||||
var configLines = DnsmasqConfFileLineParser.ParseFile(lines);
|
||||
var entries = configLines.OfType<DhcpHostLine>().Select(c => c.DhcpHost).ToList();
|
||||
foreach (var e in entries)
|
||||
{
|
||||
e.SourcePath = file.Path;
|
||||
e.IsEditable = file.IsManaged;
|
||||
}
|
||||
allEntries.AddRange(entries);
|
||||
}
|
||||
var allEntries = snapshot.DhcpHostEntries.ToList();
|
||||
AssignStableIds(allEntries);
|
||||
return allEntries;
|
||||
}
|
||||
@@ -151,6 +139,8 @@ public class DnsmasqConfigService : IDnsmasqConfigService
|
||||
await File.WriteAllLinesAsync(tmpPath, output, Encoding.UTF8, ct);
|
||||
File.Move(tmpPath, path, overwrite: true);
|
||||
EnsureManagedHostsFileExists(set.ManagedHostsFilePath);
|
||||
var effectiveHostsPathDhcp = configLines.OfType<AddnHostsLine>().FirstOrDefault()?.AddnHostsPath ?? "";
|
||||
_configSetCache.NotifyWeWroteManagedConfig(new ManagedConfigContent(configLines, effectiveHostsPathDhcp));
|
||||
_logger.LogInformation("Wrote managed config file: {Path}", path);
|
||||
}
|
||||
|
||||
@@ -179,20 +169,10 @@ public class DnsmasqConfigService : IDnsmasqConfigService
|
||||
|
||||
public async Task<ManagedConfigContent> ReadManagedConfigAsync(CancellationToken ct = default)
|
||||
{
|
||||
var path = await GetManagedFilePathAsync(ct);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
var snapshot = await _configSetCache.GetSnapshotAsync(ct);
|
||||
if (string.IsNullOrEmpty(snapshot.Set.ManagedFilePath))
|
||||
return new ManagedConfigContent(Array.Empty<DnsmasqConfLine>(), "");
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
_logger.LogWarning("Managed config file not found: {Path}", path);
|
||||
return new ManagedConfigContent(Array.Empty<DnsmasqConfLine>(), "");
|
||||
}
|
||||
|
||||
var lines = await File.ReadAllLinesAsync(path, Encoding.UTF8, ct);
|
||||
var configLines = DnsmasqConfFileLineParser.ParseFile(lines);
|
||||
var effectiveHostsPath = configLines.OfType<AddnHostsLine>().FirstOrDefault()?.AddnHostsPath ?? "";
|
||||
return new ManagedConfigContent(configLines, effectiveHostsPath);
|
||||
return snapshot.ManagedContent;
|
||||
}
|
||||
|
||||
public async Task WriteManagedConfigAsync(IReadOnlyList<DnsmasqConfLine> lines, CancellationToken ct = default)
|
||||
@@ -214,6 +194,8 @@ public class DnsmasqConfigService : IDnsmasqConfigService
|
||||
await File.WriteAllLinesAsync(tmpPath, output, Encoding.UTF8, ct);
|
||||
File.Move(tmpPath, path, overwrite: true);
|
||||
EnsureManagedHostsFileExists(set.ManagedHostsFilePath);
|
||||
var effectiveHostsPath = list.OfType<AddnHostsLine>().FirstOrDefault()?.AddnHostsPath ?? "";
|
||||
_configSetCache.NotifyWeWroteManagedConfig(new ManagedConfigContent(list, effectiveHostsPath));
|
||||
_logger.LogInformation("Wrote managed config file: {Path}", path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,231 +1,55 @@
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
|
||||
public class DnsmasqConfigSetService : IDnsmasqConfigSetService
|
||||
{
|
||||
private readonly DnsmasqOptions _options;
|
||||
private readonly IConfigSetCache _cache;
|
||||
|
||||
public DnsmasqConfigSetService(IOptions<DnsmasqOptions> options)
|
||||
public DnsmasqConfigSetService(IConfigSetCache cache)
|
||||
{
|
||||
_options = options.Value;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public Task<DnsmasqConfigSet> GetConfigSetAsync(CancellationToken ct = default) =>
|
||||
Task.FromResult(GetConfigSet());
|
||||
public async Task<DnsmasqConfigSet> GetConfigSetAsync(CancellationToken ct = default)
|
||||
{
|
||||
var snapshot = await _cache.GetSnapshotAsync(ct);
|
||||
return snapshot.Set;
|
||||
}
|
||||
|
||||
/// <summary>Leases path discovered from the config set (dhcp-leasefile= or dhcp-lease=; last wins). Null if main config missing or no directive found.</summary>
|
||||
public string? GetLeasesPath()
|
||||
{
|
||||
var set = GetConfigSet();
|
||||
if (set.Files.Count == 0)
|
||||
return null;
|
||||
var paths = set.Files.Select(f => f.Path).ToList();
|
||||
return DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(paths);
|
||||
return GetSnapshot().Config.DhcpLeaseFilePath;
|
||||
}
|
||||
|
||||
/// <summary>Effective config plus source per field so the UI can show "from X (readonly)" and why a flag cannot be unset.</summary>
|
||||
public (EffectiveDnsmasqConfig Config, EffectiveConfigSources Sources) GetEffectiveConfigWithSources()
|
||||
{
|
||||
var set = GetConfigSet();
|
||||
if (set.Files.Count == 0)
|
||||
return (CreateDefaultEffectiveConfig(), CreateDefaultEffectiveConfigSources());
|
||||
var paths = set.Files.Select(f => f.Path).ToList();
|
||||
var config = GetEffectiveConfig();
|
||||
var sources = BuildEffectiveConfigSources(paths, set.ManagedFilePath);
|
||||
return (config, sources);
|
||||
var snapshot = GetSnapshot();
|
||||
return (snapshot.Config, snapshot.Sources);
|
||||
}
|
||||
|
||||
/// <summary>Effective config from the config set (single-value and flag options; last/any wins).</summary>
|
||||
public EffectiveDnsmasqConfig GetEffectiveConfig()
|
||||
{
|
||||
var set = GetConfigSet();
|
||||
if (set.Files.Count == 0)
|
||||
return CreateDefaultEffectiveConfig();
|
||||
var paths = set.Files.Select(f => f.Path).ToList();
|
||||
|
||||
var noHosts = DnsmasqConfIncludeParser.GetNoHostsFromConfigFiles(paths);
|
||||
var addnHosts = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFiles(paths);
|
||||
var serverLocal = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, new[] { "server", "local" });
|
||||
var addressValues = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, "address");
|
||||
var interfaces = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, "interface");
|
||||
var listenAddresses = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, "listen-address");
|
||||
var exceptInterfaces = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, "except-interface");
|
||||
var dhcpRanges = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, "dhcp-range");
|
||||
var dhcpHostLines = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, "dhcp-host");
|
||||
var dhcpOptionLines = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, "dhcp-option");
|
||||
var resolvFiles = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, "resolv-file");
|
||||
|
||||
var expandHosts = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, "expand-hosts");
|
||||
var bogusPriv = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, "bogus-priv");
|
||||
var strictOrder = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, "strict-order");
|
||||
var noResolv = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, "no-resolv");
|
||||
var domainNeeded = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, "domain-needed");
|
||||
var noPoll = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, "no-poll");
|
||||
var bindInterfaces = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, "bind-interfaces");
|
||||
var noNegcache = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, "no-negcache");
|
||||
var dhcpAuthoritative = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, "dhcp-authoritative");
|
||||
var leasefileRo = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, "leasefile-ro");
|
||||
|
||||
var dhcpLeaseFilePath = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(paths);
|
||||
|
||||
var (cacheVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "cache-size");
|
||||
var cacheSize = TryParseInt(cacheVal);
|
||||
|
||||
var (portVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "port");
|
||||
var port = TryParseInt(portVal);
|
||||
|
||||
var (localTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "local-ttl");
|
||||
var localTtl = TryParseInt(localTtlVal);
|
||||
|
||||
var (pidVal, pidDir) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "pid-file");
|
||||
var pidFilePath = DnsmasqConfIncludeParser.ResolvePath(pidVal, pidDir);
|
||||
|
||||
var (userVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "user");
|
||||
var user = string.IsNullOrWhiteSpace(userVal) ? null : userVal.Trim();
|
||||
|
||||
var (groupVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "group");
|
||||
var group = string.IsNullOrWhiteSpace(groupVal) ? null : groupVal.Trim();
|
||||
|
||||
var (logFacVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "log-facility");
|
||||
var logFacility = string.IsNullOrWhiteSpace(logFacVal) ? null : logFacVal.Trim();
|
||||
|
||||
var (leaseMaxVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "dhcp-lease-max");
|
||||
var dhcpLeaseMax = TryParseInt(leaseMaxVal);
|
||||
|
||||
var (negTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "neg-ttl");
|
||||
var negTtl = TryParseInt(negTtlVal);
|
||||
|
||||
var (maxTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "max-ttl");
|
||||
var maxTtl = TryParseInt(maxTtlVal);
|
||||
|
||||
var (maxCacheTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "max-cache-ttl");
|
||||
var maxCacheTtl = TryParseInt(maxCacheTtlVal);
|
||||
|
||||
var (minCacheTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "min-cache-ttl");
|
||||
var minCacheTtl = TryParseInt(minCacheTtlVal);
|
||||
|
||||
var (dhcpTtlVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "dhcp-ttl");
|
||||
var dhcpTtl = TryParseInt(dhcpTtlVal);
|
||||
|
||||
return new EffectiveDnsmasqConfig(
|
||||
noHosts, addnHosts,
|
||||
serverLocal, addressValues, interfaces, listenAddresses, exceptInterfaces, dhcpRanges, dhcpHostLines, dhcpOptionLines, resolvFiles,
|
||||
expandHosts, bogusPriv, strictOrder, noResolv, domainNeeded, noPoll, bindInterfaces, noNegcache, dhcpAuthoritative, leasefileRo,
|
||||
dhcpLeaseFilePath, cacheSize, port, localTtl, pidFilePath, user, group, logFacility, dhcpLeaseMax,
|
||||
negTtl, maxTtl, maxCacheTtl, minCacheTtl, dhcpTtl
|
||||
);
|
||||
}
|
||||
|
||||
private static EffectiveDnsmasqConfig CreateDefaultEffectiveConfig() =>
|
||||
new(
|
||||
NoHosts: false, AddnHostsPaths: Array.Empty<string>(),
|
||||
ServerLocalValues: Array.Empty<string>(), AddressValues: Array.Empty<string>(), Interfaces: Array.Empty<string>(),
|
||||
ListenAddresses: Array.Empty<string>(), ExceptInterfaces: Array.Empty<string>(), DhcpRanges: Array.Empty<string>(),
|
||||
DhcpHostLines: Array.Empty<string>(), DhcpOptionLines: Array.Empty<string>(), ResolvFiles: Array.Empty<string>(),
|
||||
ExpandHosts: false, BogusPriv: false, StrictOrder: false, NoResolv: false, DomainNeeded: false, NoPoll: false,
|
||||
BindInterfaces: false, NoNegcache: false, DhcpAuthoritative: false, LeasefileRo: false,
|
||||
DhcpLeaseFilePath: null, CacheSize: null, Port: null, LocalTtl: null, PidFilePath: null, User: null, Group: null,
|
||||
LogFacility: null, DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null
|
||||
);
|
||||
|
||||
private static EffectiveConfigSources CreateDefaultEffectiveConfigSources() =>
|
||||
new(
|
||||
NoHosts: null, AddnHostsPaths: Array.Empty<PathWithSource>(),
|
||||
ServerLocalValues: Array.Empty<ValueWithSource>(), AddressValues: Array.Empty<ValueWithSource>(),
|
||||
Interfaces: Array.Empty<ValueWithSource>(), ListenAddresses: Array.Empty<ValueWithSource>(),
|
||||
ExceptInterfaces: Array.Empty<ValueWithSource>(), DhcpRanges: Array.Empty<ValueWithSource>(),
|
||||
DhcpHostLines: Array.Empty<ValueWithSource>(), DhcpOptionLines: Array.Empty<ValueWithSource>(),
|
||||
ResolvFiles: Array.Empty<ValueWithSource>(),
|
||||
ExpandHosts: null, BogusPriv: null, StrictOrder: null, NoResolv: null, DomainNeeded: null, NoPoll: null,
|
||||
BindInterfaces: null, NoNegcache: null, DhcpAuthoritative: null, LeasefileRo: null,
|
||||
DhcpLeaseFilePath: null, CacheSize: null, Port: null, LocalTtl: null, PidFilePath: null, User: null, Group: null,
|
||||
LogFacility: null, DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null
|
||||
);
|
||||
|
||||
private static EffectiveConfigSources BuildEffectiveConfigSources(IReadOnlyList<string> paths, string managedFilePath)
|
||||
{
|
||||
var (_, noHostsSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "no-hosts", managedFilePath);
|
||||
var addnHostsWithSource = DnsmasqConfIncludeParser.GetAddnHostsPathsFromConfigFilesWithSource(paths, managedFilePath);
|
||||
var serverLocalWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, new[] { "server", "local" }, managedFilePath);
|
||||
var addressWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, "address", managedFilePath);
|
||||
var interfacesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, "interface", managedFilePath);
|
||||
var listenAddressesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, "listen-address", managedFilePath);
|
||||
var exceptInterfacesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, "except-interface", managedFilePath);
|
||||
var dhcpRangesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, "dhcp-range", managedFilePath);
|
||||
var dhcpHostLinesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, "dhcp-host", managedFilePath);
|
||||
var dhcpOptionLinesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, "dhcp-option", managedFilePath);
|
||||
var resolvFilesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, "resolv-file", managedFilePath);
|
||||
|
||||
var (_, expandHostsSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "expand-hosts", managedFilePath);
|
||||
var (_, bogusPrivSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "bogus-priv", managedFilePath);
|
||||
var (_, strictOrderSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "strict-order", managedFilePath);
|
||||
var (_, noResolvSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "no-resolv", managedFilePath);
|
||||
var (_, domainNeededSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "domain-needed", managedFilePath);
|
||||
var (_, noPollSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "no-poll", managedFilePath);
|
||||
var (_, bindInterfacesSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "bind-interfaces", managedFilePath);
|
||||
var (_, noNegcacheSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "no-negcache", managedFilePath);
|
||||
var (_, dhcpAuthoritativeSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "dhcp-authoritative", managedFilePath);
|
||||
var (_, leasefileRoSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, "leasefile-ro", managedFilePath);
|
||||
|
||||
var (_, dhcpLeaseFilePathSource) = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFilesWithSource(paths, managedFilePath);
|
||||
var (_, cacheSizeSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "cache-size", managedFilePath);
|
||||
var (_, portSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "port", managedFilePath);
|
||||
var (_, localTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "local-ttl", managedFilePath);
|
||||
var (_, pidFilePathSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "pid-file", managedFilePath);
|
||||
var (_, userSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "user", managedFilePath);
|
||||
var (_, groupSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "group", managedFilePath);
|
||||
var (_, logFacilitySource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "log-facility", managedFilePath);
|
||||
var (_, dhcpLeaseMaxSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "dhcp-lease-max", managedFilePath);
|
||||
var (_, negTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "neg-ttl", managedFilePath);
|
||||
var (_, maxTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "max-ttl", managedFilePath);
|
||||
var (_, maxCacheTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "max-cache-ttl", managedFilePath);
|
||||
var (_, minCacheTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "min-cache-ttl", managedFilePath);
|
||||
var (_, dhcpTtlSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, "dhcp-ttl", managedFilePath);
|
||||
|
||||
return new EffectiveConfigSources(
|
||||
noHostsSource, addnHostsWithSource.Select(t => new PathWithSource(t.Path, t.Source)).ToList(),
|
||||
serverLocalWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
addressWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
interfacesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
listenAddressesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
exceptInterfacesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
dhcpRangesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
dhcpHostLinesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
dhcpOptionLinesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
resolvFilesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||
expandHostsSource, bogusPrivSource, strictOrderSource, noResolvSource, domainNeededSource, noPollSource,
|
||||
bindInterfacesSource, noNegcacheSource, dhcpAuthoritativeSource, leasefileRoSource,
|
||||
dhcpLeaseFilePathSource, cacheSizeSource, portSource, localTtlSource, pidFilePathSource, userSource, groupSource,
|
||||
logFacilitySource, dhcpLeaseMaxSource, negTtlSource, maxTtlSource, maxCacheTtlSource, minCacheTtlSource, dhcpTtlSource
|
||||
);
|
||||
}
|
||||
|
||||
private static int? TryParseInt(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
return int.TryParse(value.Trim(), out var n) ? n : null;
|
||||
return GetSnapshot().Config;
|
||||
}
|
||||
|
||||
/// <summary>Additional hosts paths discovered from the config set (addn-hosts=; cumulative). Empty list if main config missing or no addn-hosts.</summary>
|
||||
public IReadOnlyList<string> GetAddnHostsPaths()
|
||||
{
|
||||
return GetEffectiveConfig().AddnHostsPaths;
|
||||
return GetSnapshot().Config.AddnHostsPaths;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public (string? Start, string? End) GetDhcpRange()
|
||||
{
|
||||
var set = GetConfigSet();
|
||||
if (set.Files.Count == 0)
|
||||
return (null, null);
|
||||
var paths = set.Files.Select(f => f.Path).ToList();
|
||||
var (raw, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, "dhcp-range");
|
||||
return ParseDhcpRangeStartEnd(raw);
|
||||
var snapshot = GetSnapshot();
|
||||
var ranges = snapshot.Config.DhcpRanges;
|
||||
var last = ranges.Count > 0 ? ranges[ranges.Count - 1] : null;
|
||||
return ParseDhcpRangeStartEnd(last);
|
||||
}
|
||||
|
||||
/// <summary>Parses dhcp-range value to (startIp, endIp). Format is typically start,end,mask,lease or tag:...,start,end,...; finds first two IPv4-looking tokens.</summary>
|
||||
@@ -247,31 +71,6 @@ public class DnsmasqConfigSetService : IDnsmasqConfigSetService
|
||||
return (start, end);
|
||||
}
|
||||
|
||||
private DnsmasqConfigSet GetConfigSet()
|
||||
{
|
||||
var mainPath = _options.MainConfigPath;
|
||||
if (string.IsNullOrEmpty(mainPath))
|
||||
return new DnsmasqConfigSet("", "", null, Array.Empty<DnsmasqConfigSetEntry>());
|
||||
|
||||
var mainFull = Path.GetFullPath(mainPath);
|
||||
var mainDir = Path.GetDirectoryName(mainFull) ?? "";
|
||||
var managedFilePath = Path.Combine(mainDir, _options.ManagedFileName);
|
||||
var managedHostsFileName = _options.ManagedHostsFileName?.Trim();
|
||||
if (string.IsNullOrEmpty(managedHostsFileName))
|
||||
managedHostsFileName = "zz-dnsmasq-webui.hosts";
|
||||
var managedHostsFilePath = Path.Combine(mainDir, managedHostsFileName);
|
||||
|
||||
var withSource = DnsmasqConfIncludeParser.GetIncludedPathsWithSource(mainPath);
|
||||
var files = withSource.Select(p => new DnsmasqConfigSetEntry(
|
||||
p.Path,
|
||||
Path.GetFileName(p.Path),
|
||||
p.Source,
|
||||
IsManaged: string.Equals(p.Path, managedFilePath, StringComparison.Ordinal)
|
||||
)).ToList();
|
||||
|
||||
if (files.All(e => !string.Equals(e.Path, managedFilePath, StringComparison.Ordinal)))
|
||||
files.Add(new DnsmasqConfigSetEntry(managedFilePath, Path.GetFileName(managedFilePath), DnsmasqConfFileSource.ConfFile, IsManaged: true));
|
||||
|
||||
return new DnsmasqConfigSet(mainFull, managedFilePath, managedHostsFilePath, files);
|
||||
}
|
||||
private ConfigSetSnapshot GetSnapshot() =>
|
||||
_cache.GetSnapshotAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
using System.Text;
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton cache for hosts: managed hosts file + read-only (system + addn-hosts). Invalidates on watchers (managed + system path), staleness, or Invalidate.
|
||||
/// Call NotifyWeWroteManagedHosts after the app writes the managed hosts file so the cache updates in place and ignores the next watcher event.
|
||||
/// </summary>
|
||||
public sealed class HostsCache : IHostsCache, IDisposable
|
||||
{
|
||||
private const int StaleCacheSeconds = 120;
|
||||
private const double SelfWriteIgnoreSeconds = 1.5;
|
||||
|
||||
private readonly IConfigSetCache _configSetCache;
|
||||
private readonly DnsmasqOptions _options;
|
||||
private readonly ILogger<HostsCache> _logger;
|
||||
private readonly object _lock = new();
|
||||
private HostsSnapshot? _snapshot;
|
||||
private DateTime? _lastReadUtc;
|
||||
private bool _dirty = true;
|
||||
private DateTime _lastWriteManagedUtc = DateTime.MinValue;
|
||||
private string? _lastManagedHostsPath;
|
||||
private FileSystemWatcher? _watcherManaged;
|
||||
private FileSystemWatcher? _watcherSystem;
|
||||
|
||||
public HostsCache(IConfigSetCache configSetCache, IOptions<DnsmasqOptions> options, ILogger<HostsCache> logger)
|
||||
{
|
||||
_configSetCache = configSetCache;
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Invalidate()
|
||||
{
|
||||
lock (_lock)
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
public void NotifyWeWroteManagedHosts(IReadOnlyList<HostEntry> entries)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_lastWriteManagedUtc = DateTime.UtcNow;
|
||||
if (_snapshot != null)
|
||||
_snapshot = _snapshot with { ManagedEntries = entries.ToList() };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<HostsSnapshot> GetSnapshotAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await Task.Run(() => GetSnapshot(ct), ct);
|
||||
}
|
||||
|
||||
private HostsSnapshot GetSnapshot(CancellationToken ct)
|
||||
{
|
||||
var configSnapshot = _configSetCache.GetSnapshotAsync(ct).GetAwaiter().GetResult();
|
||||
var set = configSnapshot.Set;
|
||||
var effectiveConfig = configSnapshot.Config;
|
||||
var managedPath = set.ManagedHostsFilePath;
|
||||
var addnPaths = effectiveConfig.AddnHostsPaths ?? Array.Empty<string>();
|
||||
var noHosts = effectiveConfig.NoHosts;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_dirty && _snapshot != null && _lastReadUtc.HasValue)
|
||||
{
|
||||
var ageSeconds = (DateTime.UtcNow - _lastReadUtc.Value).TotalSeconds;
|
||||
if (ageSeconds < StaleCacheSeconds)
|
||||
return _snapshot;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
TryEnsureWatchers(managedPath);
|
||||
|
||||
var managedEntries = ReadManagedEntries(managedPath);
|
||||
var readOnlyFiles = ReadReadOnlyFiles(managedPath, addnPaths, noHosts, ct);
|
||||
|
||||
_lastManagedHostsPath = !string.IsNullOrEmpty(managedPath) ? Path.GetFullPath(managedPath) : null;
|
||||
_snapshot = new HostsSnapshot(managedEntries, readOnlyFiles);
|
||||
_lastReadUtc = DateTime.UtcNow;
|
||||
_dirty = false;
|
||||
return _snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryEnsureWatchers(string? managedPath)
|
||||
{
|
||||
if (_watcherManaged != null && _watcherSystem != null)
|
||||
return;
|
||||
|
||||
var managedPathFull = !string.IsNullOrEmpty(managedPath) ? Path.GetFullPath(managedPath) : null;
|
||||
if (!string.IsNullOrEmpty(managedPathFull))
|
||||
{
|
||||
var dir = Path.GetDirectoryName(managedPathFull);
|
||||
var fileName = Path.GetFileName(managedPathFull);
|
||||
if (!string.IsNullOrEmpty(dir) && !string.IsNullOrEmpty(fileName) && Directory.Exists(dir))
|
||||
{
|
||||
try
|
||||
{
|
||||
_watcherManaged ??= CreateWatcher(dir, fileName, OnFileChanged);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not create watcher for managed hosts: {Path}", managedPathFull);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var systemPath = _options.SystemHostsPath?.Trim();
|
||||
var systemPathFull = !string.IsNullOrEmpty(systemPath) ? Path.GetFullPath(systemPath) : null;
|
||||
if (!string.IsNullOrEmpty(systemPathFull))
|
||||
{
|
||||
var dir = Path.GetDirectoryName(systemPathFull);
|
||||
var fileName = Path.GetFileName(systemPathFull);
|
||||
if (!string.IsNullOrEmpty(dir) && !string.IsNullOrEmpty(fileName) && Directory.Exists(dir))
|
||||
{
|
||||
try
|
||||
{
|
||||
_watcherSystem ??= CreateWatcher(dir, fileName, OnFileChanged);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not create watcher for system hosts: {Path}", systemPathFull);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static FileSystemWatcher CreateWatcher(string dir, string fileName, FileSystemEventHandler onChanged)
|
||||
{
|
||||
var w = new FileSystemWatcher(dir)
|
||||
{
|
||||
Filter = fileName,
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size
|
||||
};
|
||||
w.Changed += onChanged;
|
||||
w.Created += onChanged;
|
||||
w.EnableRaisingEvents = true;
|
||||
return w;
|
||||
}
|
||||
|
||||
private void OnFileChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(e.FullPath);
|
||||
lock (_lock)
|
||||
{
|
||||
if (_lastManagedHostsPath != null && string.Equals(fullPath, _lastManagedHostsPath, StringComparison.Ordinal))
|
||||
{
|
||||
var elapsed = (DateTime.UtcNow - _lastWriteManagedUtc).TotalSeconds;
|
||||
if (elapsed < SelfWriteIgnoreSeconds)
|
||||
return;
|
||||
}
|
||||
_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<HostEntry> ReadManagedEntries(string? managedPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(managedPath) || !File.Exists(managedPath))
|
||||
return new List<HostEntry>();
|
||||
|
||||
var lines = File.ReadAllLines(managedPath, Encoding.UTF8);
|
||||
var entries = new List<HostEntry>();
|
||||
var seenContentIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var entry = HostsFileLineParser.ParseLine(lines[i], i + 1);
|
||||
if (entry == null) continue;
|
||||
if (entry.IsPassthrough || string.IsNullOrEmpty(entry.Address))
|
||||
entry.Id = "line:" + entry.LineNumber;
|
||||
else
|
||||
{
|
||||
var contentId = entry.Address + "|" + string.Join(",", entry.Names.OrderBy(x => x, StringComparer.Ordinal));
|
||||
entry.Id = seenContentIds.Add(contentId) ? contentId : contentId + ":" + entry.LineNumber;
|
||||
}
|
||||
entries.Add(entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private List<ReadOnlyHostsFile> ReadReadOnlyFiles(string? managedPath, IReadOnlyList<string> addnPaths, bool noHosts, CancellationToken ct)
|
||||
{
|
||||
var result = new List<ReadOnlyHostsFile>();
|
||||
var managedPathFull = !string.IsNullOrEmpty(managedPath) ? Path.GetFullPath(managedPath) : null;
|
||||
var systemPath = _options.SystemHostsPath?.Trim();
|
||||
var systemPathFull = !string.IsNullOrEmpty(systemPath) ? Path.GetFullPath(systemPath) : null;
|
||||
|
||||
if (!noHosts && !string.IsNullOrEmpty(systemPathFull) && File.Exists(systemPathFull))
|
||||
{
|
||||
try
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var lines = File.ReadAllLines(systemPathFull, Encoding.UTF8);
|
||||
var entries = new List<HostEntry>();
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var entry = HostsFileLineParser.ParseLine(lines[i], i + 1);
|
||||
if (entry != null)
|
||||
entries.Add(entry);
|
||||
}
|
||||
result.Add(new ReadOnlyHostsFile(systemPathFull, entries));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip unreadable
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var p in addnPaths)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var fullPath = Path.GetFullPath(p);
|
||||
if (managedPathFull != null && string.Equals(fullPath, managedPathFull, StringComparison.Ordinal))
|
||||
continue;
|
||||
if (systemPathFull != null && string.Equals(fullPath, systemPathFull, StringComparison.Ordinal))
|
||||
continue;
|
||||
if (!File.Exists(fullPath))
|
||||
continue;
|
||||
try
|
||||
{
|
||||
var lines = File.ReadAllLines(fullPath, Encoding.UTF8);
|
||||
var entries = new List<HostEntry>();
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var entry = HostsFileLineParser.ParseLine(lines[i], i + 1);
|
||||
if (entry != null)
|
||||
entries.Add(entry);
|
||||
}
|
||||
result.Add(new ReadOnlyHostsFile(fullPath, entries));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip unreadable
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_watcherManaged?.Dispose();
|
||||
_watcherManaged = null;
|
||||
_watcherSystem?.Dispose();
|
||||
_watcherSystem = null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Text;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
@@ -9,42 +8,20 @@ namespace DnsmasqWebUI.Services;
|
||||
public class HostsFileService : IHostsFileService
|
||||
{
|
||||
private readonly IDnsmasqConfigSetService _configSetService;
|
||||
private readonly IHostsCache _hostsCache;
|
||||
private readonly ILogger<HostsFileService> _logger;
|
||||
|
||||
public HostsFileService(IDnsmasqConfigSetService configSetService, ILogger<HostsFileService> logger)
|
||||
public HostsFileService(IDnsmasqConfigSetService configSetService, IHostsCache hostsCache, ILogger<HostsFileService> logger)
|
||||
{
|
||||
_configSetService = configSetService;
|
||||
_hostsCache = hostsCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<HostEntry>> ReadAsync(CancellationToken ct = default)
|
||||
{
|
||||
var set = await _configSetService.GetConfigSetAsync(ct);
|
||||
var path = set.ManagedHostsFilePath;
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return Array.Empty<HostEntry>();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
_logger.LogDebug("Managed hosts file not found: {Path}", path);
|
||||
return Array.Empty<HostEntry>();
|
||||
}
|
||||
var lines = await File.ReadAllLinesAsync(path, Encoding.UTF8, ct);
|
||||
var entries = new List<HostEntry>();
|
||||
var seenContentIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
for (var i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var entry = HostsFileLineParser.ParseLine(lines[i], i + 1);
|
||||
if (entry == null) continue;
|
||||
if (entry.IsPassthrough || string.IsNullOrEmpty(entry.Address))
|
||||
entry.Id = "line:" + entry.LineNumber;
|
||||
else
|
||||
{
|
||||
var contentId = entry.Address + "|" + string.Join(",", entry.Names.OrderBy(x => x, StringComparer.Ordinal));
|
||||
entry.Id = seenContentIds.Add(contentId) ? contentId : contentId + ":" + entry.LineNumber;
|
||||
}
|
||||
entries.Add(entry);
|
||||
}
|
||||
return entries;
|
||||
var snapshot = await _hostsCache.GetSnapshotAsync(ct);
|
||||
return snapshot.ManagedEntries;
|
||||
}
|
||||
|
||||
public async Task WriteAsync(IReadOnlyList<HostEntry> entries, CancellationToken ct = default)
|
||||
@@ -61,6 +38,7 @@ public class HostsFileService : IHostsFileService
|
||||
var lines = entries.Select(HostsFileLineParser.ToLine).ToList();
|
||||
await File.WriteAllLinesAsync(tmpPath, lines, Encoding.UTF8, ct);
|
||||
File.Move(tmpPath, path, overwrite: true);
|
||||
_hostsCache.NotifyWeWroteManagedHosts(entries);
|
||||
_logger.LogInformation("Wrote managed hosts file: {Path}", path);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user