Docs: XML comments for interfaces/models and README codebase overview

- Add XML summaries and params to service/client interfaces (IDnsmasqConfigService, IHostsFileService, ILeasesFileService, IReloadService, ILeasesCache, IStatusClient, IConfigSetClient, IReloadClient, IHostsClient, IDhcpHostsClient, ILeasesClient)
- Add <param> to Models/Contracts and Dnsmasq/EffectiveConfig records (ConfigSetSnapshot, HostsSnapshot, ManagedConfigContent, SaveWithReloadResult, ReloadResult, DnsmasqConfigSet, DnsmasqConfigSetEntry, LeasesResult, ReadOnlyHostsFile)
- Document HostEntry, DhcpHostEntry, LeaseEntry with type and property summaries
- Update README codebase overview to reflect refactored layout (Infrastructure/, Models/Config, Models/Contracts, Models/Dnsmasq, etc.)
This commit is contained in:
2026-02-05 23:15:47 +10:00
parent b44dde2c77
commit 3ae556168e
23 changed files with 106 additions and 3 deletions
+3 -3
View File
@@ -477,9 +477,9 @@ sudo ./scripts/install.sh --uninstall --purge --system
**Codebase overview:**
- **Entry and config:** `Program.cs` wires the host; configuration (e.g. `Dnsmasq__*`) is in `Configuration/` (`DnsmasqOptions`, `ApplicationOptions`). Options are validated at startup.
- **Config and hosts:** Services in `Services/` and `Services/Abstractions/` read and write dnsmasq config and hosts. A **managed** config file and optional managed hosts file are written by the app; the main config must include the managed file (e.g. via a final `conf-file=` line). Caches (`ConfigSetCache`, `HostsCache`) keep in-memory snapshots and refresh on file changes or staleness.
- **Parsers and models:** `Parsers/` parses dnsmasq config lines and files (`DnsmasqConfIncludeParser`, `DnsmasqConfDirectiveParser`, etc.). Models in `Models/` represent config sets, config, sources, DHCP entries, hosts, leases.
- **Entry and config:** `Program.cs` wires the host; configuration (e.g. `Dnsmasq__*`) lives in `Models/Config/` (`DnsmasqOptions`, `ApplicationOptions`, `DnsmasqOptionsValidator`). Options are validated at startup.
- **Infrastructure:** Under `Infrastructure/`: **Client/** (HTTP API clients and `Abstractions/`), **Services/** and **Services/Abstractions/** (caches, config/hosts/leases services, reload, hosted services), **Parsers/** (dnsmasq config and hosts line parsers), **Helpers/Config/** and **Helpers/Http/** (option keys, encoding, tooltips, JSON options). A **managed** config file and optional managed hosts file are written by the app; the main config must include the managed file (e.g. via a final `conf-file=` line). Caches (`ConfigSetCache`, `HostsCache`) keep in-memory snapshots and refresh on file changes or staleness.
- **Models:** `Models/Config/` (options, validators, `DnsmasqConfLine`); `Models/Contracts/` (snapshots and DTOs such as `ConfigSetSnapshot`, `HostsSnapshot`, `ManagedConfigContent`, `ProcessRunResult`, `ReloadResult`); `Models/Dnsmasq/` (status, `SaveWithReloadResult`, and `EffectiveConfig/` for effective config and sources); `Models/Client/`, `Models/Dhcp/`, `Models/Hosts/` (UI and API DTOs).
- **API and UI:** `Controllers/` expose API endpoints; `Components/` contains Blazor Server components for config editor, hosts, DHCP, Dnsmasq status, and app settings. Static assets in `wwwroot/`.
- **Tests:** `DnsmasqWebUI.Tests` contains unit tests for parsers, config set service, config, and related logic. Run with `dotnet test`.
@@ -5,5 +5,6 @@ namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for GET api/config/set.</summary>
public interface IConfigSetClient
{
/// <summary>Gets the config set (main + conf-file/conf-dir) and managed file path from GET api/config/set.</summary>
Task<DnsmasqConfigSet> GetConfigSetAsync(CancellationToken ct = default);
}
@@ -7,6 +7,9 @@ namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for GET/PUT api/dhcp/hosts.</summary>
public interface IDhcpHostsClient
{
/// <summary>Gets DHCP host entries from GET api/dhcp/hosts.</summary>
Task<IReadOnlyList<DhcpHostEntry>> GetDhcpHostsAsync(CancellationToken ct = default);
/// <summary>Writes DHCP host entries and triggers reload via PUT api/dhcp/hosts.</summary>
Task<SaveWithReloadResult> SaveDhcpHostsAsync(IReadOnlyList<DhcpHostEntry> entries, CancellationToken ct = default);
}
@@ -7,7 +7,12 @@ namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for GET/PUT api/hosts.</summary>
public interface IHostsClient
{
/// <summary>Gets managed hosts file entries from GET api/hosts.</summary>
Task<IReadOnlyList<HostEntry>> GetHostsAsync(CancellationToken ct = default);
/// <summary>Gets read-only hosts files (system + addn-hosts) from GET api/hosts/readonly.</summary>
Task<IReadOnlyList<ReadOnlyHostsFile>> GetReadOnlyHostsAsync(CancellationToken ct = default);
/// <summary>Writes managed hosts file and triggers reload via PUT api/hosts.</summary>
Task<SaveWithReloadResult> SaveHostsAsync(IReadOnlyList<HostEntry> entries, CancellationToken ct = default);
}
@@ -5,6 +5,7 @@ namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for GET api/leases.</summary>
public interface ILeasesClient
{
/// <summary>Gets DHCP lease entries from GET api/leases. Optionally forces a cache refresh on the server.</summary>
/// <param name="forceRefresh">When true, invalidates the server cache so the next read is from disk (e.g. after manual Refresh).</param>
Task<LeasesResult> GetLeasesAsync(bool forceRefresh = false, CancellationToken ct = default);
}
@@ -5,5 +5,6 @@ namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for POST api/reload.</summary>
public interface IReloadClient
{
/// <summary>Triggers dnsmasq reload via POST api/reload.</summary>
Task<ReloadResult> ReloadAsync(CancellationToken ct = default);
}
@@ -5,5 +5,6 @@ namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for GET api/status.</summary>
public interface IStatusClient
{
/// <summary>Gets dnsmasq and config status from GET api/status.</summary>
Task<DnsmasqServiceStatus> GetStatusAsync(CancellationToken ct = default);
}
@@ -6,10 +6,18 @@ using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>Reads and writes the managed dnsmasq config file and DHCP hosts entries (the app-managed config and dhcp-host= lines).</summary>
public interface IDnsmasqConfigService : IApplicationScopedService
{
/// <summary>Reads DHCP host entries from the managed config (dhcp-host= lines).</summary>
Task<IReadOnlyList<DhcpHostEntry>> ReadDhcpHostsAsync(CancellationToken ct = default);
/// <summary>Writes DHCP host entries to the managed config file (replaces dhcp-host= lines).</summary>
Task WriteDhcpHostsAsync(IReadOnlyList<DhcpHostEntry> entries, CancellationToken ct = default);
/// <summary>Reads the full managed config file as structured lines plus the effective addn-hosts path in file (for display).</summary>
Task<ManagedConfigContent> ReadManagedConfigAsync(CancellationToken ct = default);
/// <summary>Writes the full managed config file from the given lines (round-trip from ReadManagedConfigAsync).</summary>
Task WriteManagedConfigAsync(IReadOnlyList<DnsmasqConfLine> lines, CancellationToken ct = default);
}
@@ -2,8 +2,12 @@ using DnsmasqWebUI.Models.Hosts;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>Reads and writes the app-managed hosts file (the single hosts file the app edits).</summary>
public interface IHostsFileService : IApplicationScopedService
{
/// <summary>Reads all entries from the managed hosts file (including comments and passthrough lines).</summary>
Task<IReadOnlyList<HostEntry>> ReadAsync(CancellationToken ct = default);
/// <summary>Writes the given entries to the managed hosts file (replaces file content).</summary>
Task WriteAsync(IReadOnlyList<HostEntry> entries, CancellationToken ct = default);
}
@@ -10,5 +10,6 @@ public interface ILeasesCache : IApplicationSingleton
/// <summary>Forces the next <see cref="GetOrRefreshAsync"/> to re-read the file. Use for manual Refresh; no need to recreate the file watcher.</summary>
void Invalidate();
/// <summary>Returns cached lease entries, or re-reads from disk if invalidated or file changed.</summary>
Task<(bool Available, IReadOnlyList<LeaseEntry>? Entries)> GetOrRefreshAsync(CancellationToken ct = default);
}
@@ -2,8 +2,12 @@ using DnsmasqWebUI.Models.Dhcp;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>Reads the dnsmasq DHCP leases file (path from effective config).</summary>
public interface ILeasesFileService : IApplicationScopedService
{
/// <summary>Reads lease entries from the leases file. Throws if file is not configured or not readable.</summary>
Task<IReadOnlyList<LeaseEntry>> ReadAsync(CancellationToken ct = default);
/// <summary>Attempts to read the leases file. Returns (false, null) when not configured or not readable; (true, entries) otherwise.</summary>
Task<(bool Available, IReadOnlyList<LeaseEntry>? Entries)> TryReadAsync(CancellationToken ct = default);
}
@@ -1,8 +1,15 @@
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>Runs the configured dnsmasq reload command (e.g. systemctl reload dnsmasq).</summary>
public interface IReloadService : IApplicationScopedService
{
/// <summary>Executes the reload command and returns exit code and output. Serialised so only one reload runs at a time.</summary>
Task<ReloadResult> ReloadAsync(CancellationToken ct = default);
}
/// <summary>Result of running the reload command.</summary>
/// <param name="Success">True when the command exited with code 0.</param>
/// <param name="ExitCode">Process exit code; -1 when failed to start or timed out.</param>
/// <param name="StdOut">Standard output from the command.</param>
/// <param name="StdErr">Standard error from the command.</param>
public record ReloadResult(bool Success, int ExitCode, string? StdOut, string? StdErr);
@@ -5,6 +5,11 @@ using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Models.Contracts;
/// <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>
/// <param name="Set">Ordered config set (main + conf-file/conf-dir) and managed file paths.</param>
/// <param name="Config">Effective dnsmasq config built from all files.</param>
/// <param name="Sources">Source per field (file path, readonly) for UI tooltips.</param>
/// <param name="ManagedContent">Parsed lines and effective addn-hosts path of the managed config file.</param>
/// <param name="DhcpHostEntries">DHCP host entries read from the managed file.</param>
public record ConfigSetSnapshot(
DnsmasqConfigSet Set,
EffectiveDnsmasqConfig Config,
@@ -3,6 +3,8 @@ using DnsmasqWebUI.Models.Hosts;
namespace DnsmasqWebUI.Models.Contracts;
/// <summary>Immutable snapshot from the hosts cache: managed hosts file entries and read-only hosts files (system + addn-hosts).</summary>
/// <param name="ManagedEntries">Entries from the app-managed hosts file.</param>
/// <param name="ReadOnlyFiles">Read-only hosts files (system hosts when configured, then addn-hosts that are not the managed file).</param>
public record HostsSnapshot(
IReadOnlyList<HostEntry> ManagedEntries,
IReadOnlyList<ReadOnlyHostsFile> ReadOnlyFiles
@@ -3,4 +3,6 @@ using DnsmasqWebUI.Models.Config;
namespace DnsmasqWebUI.Models.Contracts;
/// <summary>Full managed file content. EffectiveHostsPathInFile is parsed from AddnHosts line if present (for display only).</summary>
/// <param name="Lines">Parsed config lines (blank, comment, addn-hosts, dhcp-host, other).</param>
/// <param name="EffectiveHostsPathInFile">Path from addn-hosts= in the managed file, for display; empty when not set.</param>
public record ManagedConfigContent(IReadOnlyList<DnsmasqConfLine> Lines, string EffectiveHostsPathInFile);
@@ -1,21 +1,42 @@
namespace DnsmasqWebUI.Models.Dhcp;
/// <summary>One dhcp-host= line (MAC(s), optional address, name, lease, options). Used for GET/PUT api/dhcp/hosts.</summary>
public class DhcpHostEntry
{
/// <summary>1-based line number in the config file. Used for display and matching.</summary>
public int LineNumber { get; set; }
/// <summary>Stable identifier (content-based: MACs|Address|Name, with ":LineNumber" for uniqueness). Set by server on GET; used to match entries on PUT so reordering is safe.</summary>
public string Id { get; set; } = "";
/// <summary>Original line text as read from the config (for passthrough and round-trip).</summary>
public string RawLine { get; set; } = "";
/// <summary>True when the line is a comment (starts with #).</summary>
public bool IsComment { get; set; }
/// <summary>True when the entry was marked for deletion in the UI (removed on save).</summary>
public bool IsDeleted { get; set; }
/// <summary>True when the line could not be parsed (preserved as RawLine on write).</summary>
public bool Ignore { get; set; }
/// <summary>MAC address(es) from the dhcp-host line. One or more; order preserved.</summary>
public List<string> MacAddresses { get; set; } = new();
/// <summary>Hostname or DHCP hostname from the line; null when not set.</summary>
public string? Name { get; set; }
/// <summary>Reserved IP address from the line; null when not set.</summary>
public string? Address { get; set; }
/// <summary>Lease identifier (e.g. "01:02:03:04:05:06"); null when not set.</summary>
public string? Lease { get; set; }
/// <summary>Extra tokens after the main dhcp-host fields (e.g. set:name); preserved on write.</summary>
public List<string> Extra { get; set; } = new();
/// <summary>Inline comment text from the line; null when none.</summary>
public string? Comment { get; set; }
/// <summary>True when this entry is from the managed file (editable). False when from main config or another file (read-only). Set by server on GET; new entries created in the UI should set this to true.</summary>
@@ -1,11 +1,23 @@
namespace DnsmasqWebUI.Models.Dhcp;
/// <summary>One DHCP lease entry from the dnsmasq leases file (timestamp, MAC, IP, hostname, client-id).</summary>
public class LeaseEntry
{
/// <summary>Lease expiry time as Unix timestamp (seconds since epoch).</summary>
public long Epoch { get; set; }
/// <summary>Client MAC address.</summary>
public string Mac { get; set; } = "";
/// <summary>Assigned IP address.</summary>
public string Address { get; set; } = "";
/// <summary>Hostname from DHCP or DNS; empty when unknown.</summary>
public string Name { get; set; } = "";
/// <summary>DHCP client identifier; empty when not set.</summary>
public string ClientId { get; set; } = "";
/// <summary>Lease expiry as DateTime (derived from Epoch).</summary>
public DateTime Timestamp => DateTimeOffset.FromUnixTimeSeconds(Epoch).DateTime;
}
@@ -1,4 +1,7 @@
namespace DnsmasqWebUI.Models.Dhcp;
/// <summary>Result of GET api/leases: whether leases are available and the list of entries.</summary>
/// <param name="Available">True when the leases file is configured and readable; false when not configured or path invalid.</param>
/// <param name="Entries">Lease entries; null when not available or file unreadable (Message explains).</param>
/// <param name="Message">Error or status message (e.g. "Leases not configured."); null when successful.</param>
public record LeasesResult(bool Available, IReadOnlyList<LeaseEntry>? Entries, string? Message);
@@ -1,6 +1,10 @@
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
/// <summary>Ordered set of dnsmasq config files (main + conf-file + conf-dir). ManagedFilePath is the single config file we read/write; ManagedHostsFilePath is the single hosts file we read/write.</summary>
/// <param name="MainConfigPath">Path to the main dnsmasq config file (e.g. /etc/dnsmasq.conf).</param>
/// <param name="ManagedFilePath">Path to the app-managed config file (e.g. zz-dnsmasq-webui.conf).</param>
/// <param name="ManagedHostsFilePath">Path to the app-managed hosts file; null when not configured.</param>
/// <param name="Files">Ordered list of config files (main + included) for display.</param>
public record DnsmasqConfigSet(
string MainConfigPath,
string ManagedFilePath,
@@ -1,4 +1,8 @@
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
/// <summary>One file in the dnsmasq config set (main or included). IsManaged is true only for the app-managed file.</summary>
/// <param name="Path">Absolute path of the config file.</param>
/// <param name="FileName">Filename only (e.g. dnsmasq.conf, 01-default.conf).</param>
/// <param name="Source">Whether the file is main, conf-file, or conf-dir.</param>
/// <param name="IsManaged">True when this file is the app-managed config file (editable from UI).</param>
public record DnsmasqConfigSetEntry(string Path, string FileName, DnsmasqConfFileSource Source, bool IsManaged);
@@ -3,4 +3,6 @@ using DnsmasqWebUI.Infrastructure.Services.Abstractions;
namespace DnsmasqWebUI.Models.Dnsmasq;
/// <summary>Result of a save operation that triggers a dnsmasq reload (e.g. PUT api/hosts, PUT api/dhcp/hosts).</summary>
/// <param name="Saved">True when the write succeeded (reload result may still indicate failure).</param>
/// <param name="Reload">Result of the reload command run after save.</param>
public record SaveWithReloadResult(bool Saved, ReloadResult Reload);
@@ -1,5 +1,6 @@
namespace DnsmasqWebUI.Models.Hosts;
/// <summary>One line or entry in an /etc/hosts-style file (IP, names, comment, or passthrough).</summary>
public class HostEntry
{
/// <summary>1-based line number in the file. Used for display and as fallback when Id is not set.</summary>
@@ -8,9 +9,18 @@ public class HostEntry
/// <summary>Stable identifier: host entries use "Address|name1,name2" (content-based, stable across reorder); passthrough use "line:LineNumber". Set by server on GET; optional on PUT (order is preserved).</summary>
public string Id { get; set; } = "";
/// <summary>IP address (e.g. 192.168.1.1). Empty for comment or passthrough lines.</summary>
public string Address { get; set; } = "";
/// <summary>Canonical hostname and aliases. Order preserved.</summary>
public List<string> Names { get; set; } = new();
/// <summary>Original line text as read from the file (for passthrough and round-trip).</summary>
public string RawLine { get; set; } = "";
/// <summary>True when the line is a comment (starts with #).</summary>
public bool IsComment { get; set; }
/// <summary>True when the line could not be parsed as address/names (e.g. malformed or comment); preserved as RawLine on write.</summary>
public bool IsPassthrough { get; set; }
}
@@ -1,4 +1,6 @@
namespace DnsmasqWebUI.Models.Hosts;
/// <summary>Path and parsed entries for a read-only addn-hosts file (not the managed hosts file).</summary>
/// <param name="Path">Absolute path of the hosts file (e.g. /etc/hosts or an addn-hosts path).</param>
/// <param name="Entries">Parsed entries from the file (IP, names, comments, passthrough).</param>
public record ReadOnlyHostsFile(string Path, IReadOnlyList<HostEntry> Entries);