Refactor: reorganise namespaces and move service DTOs to Models/Contracts

- Move Client/Http, Services, Parsers under Infrastructure/ (Client, Services, Parsers)
- Move config helpers (DnsmasqConfKeys, DnsmasqOptionTooltips, DnsmasqFileEncoding, EffectiveConfigFieldBuilder) to Infrastructure/Helpers/Config; ApiJsonOptions to Helpers/Http
- Move Configuration types (ApplicationOptions, DnsmasqOptions, DnsmasqOptionsValidator) to Models/Config
- Merge Models/Status and Models/EffectiveConfig into Models/Dnsmasq and Models/Dnsmasq/EffectiveConfig
- Move Client/Models and Client/Services to Models/Client and Infrastructure/Services
- Move ConfigSetSnapshot, HostsSnapshot, ProcessRunResult, ManagedConfigContent from Infrastructure/Services/Abstractions to Models/Contracts
- Update all namespaces and usings; tests pass
This commit is contained in:
2026-02-05 23:08:40 +10:00
parent 23bf6bb9e0
commit b44dde2c77
106 changed files with 377 additions and 271 deletions
@@ -1,4 +1,4 @@
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Tests;
@@ -1,5 +1,5 @@
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Infrastructure.Parsers;
namespace DnsmasqWebUI.Tests;
@@ -1,5 +1,5 @@
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Infrastructure.Parsers;
namespace DnsmasqWebUI.Tests;
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Infrastructure.Parsers;
namespace DnsmasqWebUI.Tests;
@@ -1,5 +1,7 @@
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using DnsmasqWebUI.Infrastructure.Parsers;
namespace DnsmasqWebUI.Tests;
@@ -145,14 +147,57 @@ public class DnsmasqConfIncludeParserTests
}
[Fact]
public void GetIncludedPaths_testdata_dnsmasq_conf_ReturnsMainOnlyWhenConfDirMissing()
public void GetIncludedPaths_ConfDirMissing_ReturnsMainOnly()
{
var mainPath = TestDataHelper.GetPath("dnsmasq.conf");
if (!File.Exists(mainPath))
return; // testdata not copied
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(mainPath);
Assert.Single(paths);
Assert.Equal(Path.GetFullPath(mainPath), paths[0]);
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-confdir-missing-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(baseDir);
try
{
var main = Path.Combine(baseDir, "dnsmasq.conf");
File.WriteAllText(main, "conf-dir=nonexistent-subdir\n");
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
Assert.Single(paths);
Assert.Equal(Path.GetFullPath(main), paths[0]);
}
finally
{
Directory.Delete(baseDir, recursive: true);
}
}
/// <summary>Mirrors harness layout: main + conf-dir with multiple files (including lan-dns.conf) + conf-file. Asserts all appear in load order with correct sources.</summary>
[Fact]
public void GetIncludedPathsWithSource_ConfDirAndConfFile_ReturnsAllFilesInLoadOrderWithSources()
{
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-harness-layout-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(baseDir);
var confDir = Path.Combine(baseDir, "dnsmasq.d");
Directory.CreateDirectory(confDir);
try
{
File.WriteAllText(Path.Combine(confDir, "01-other.conf"), "# other\n");
File.WriteAllText(Path.Combine(confDir, "02-servers.conf"), "# servers\n");
File.WriteAllText(Path.Combine(confDir, "lan-dns.conf"), "# lan dns\n");
File.WriteAllText(Path.Combine(baseDir, "zz-dnsmasq-webui.conf"), "# managed\n");
var main = Path.Combine(baseDir, "dnsmasq.conf");
File.WriteAllText(main, "conf-dir=dnsmasq.d\nconf-file=zz-dnsmasq-webui.conf\n");
var withSource = DnsmasqConfIncludeParser.GetIncludedPathsWithSource(main);
Assert.Equal(5, withSource.Count);
Assert.Equal(Path.GetFullPath(main), withSource[0].Path);
Assert.Equal(DnsmasqConfFileSource.Main, withSource[0].Source);
Assert.Equal(Path.GetFullPath(Path.Combine(confDir, "01-other.conf")), withSource[1].Path);
Assert.Equal(DnsmasqConfFileSource.ConfDir, withSource[1].Source);
Assert.Equal(Path.GetFullPath(Path.Combine(confDir, "02-servers.conf")), withSource[2].Path);
Assert.Equal(DnsmasqConfFileSource.ConfDir, withSource[2].Source);
Assert.Equal(Path.GetFullPath(Path.Combine(confDir, "lan-dns.conf")), withSource[3].Path);
Assert.Equal(DnsmasqConfFileSource.ConfDir, withSource[3].Source);
Assert.Equal(Path.GetFullPath(Path.Combine(baseDir, "zz-dnsmasq-webui.conf")), withSource[4].Path);
Assert.Equal(DnsmasqConfFileSource.ConfFile, withSource[4].Source);
}
finally
{
Directory.Delete(baseDir, recursive: true);
}
}
[Fact]
@@ -1,6 +1,7 @@
using DnsmasqWebUI.Configuration;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Services;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using DnsmasqWebUI.Infrastructure.Services;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@@ -1,5 +1,5 @@
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Infrastructure.Parsers;
namespace DnsmasqWebUI.Tests;
@@ -1,4 +1,4 @@
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Tests;
@@ -1,5 +1,5 @@
using DnsmasqWebUI.Models.Hosts;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Infrastructure.Parsers;
namespace DnsmasqWebUI.Tests;
@@ -1,24 +0,0 @@
using System.Text.Json;
using DnsmasqWebUI.Client.Http.Abstractions;
using DnsmasqWebUI.Models.Status;
using System.Net.Http.Json;
namespace DnsmasqWebUI.Client.Http;
public sealed class StatusClient : IStatusClient
{
private readonly HttpClient _http;
/// <summary>Matches API serialization (camelCase) so nested EffectiveConfigSources and tuple (value, source) deserialize correctly.</summary>
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
public StatusClient(HttpClient http) => _http = http;
public async Task<DnsmasqServiceStatus> GetStatusAsync(CancellationToken ct = default) =>
await _http.GetFromJsonAsync<DnsmasqServiceStatus>("api/status", JsonOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/status.");
}
@@ -1,5 +1,5 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays
@using DnsmasqWebUI.Models.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
@* Reusable value-only display for boolean/flag options. Edit mode: checkbox (unless readonly); View: "Enabled" / "Disabled". *@
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Descriptor.GetSource()?.IsReadOnly != true)
@@ -1,5 +1,5 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays
@using DnsmasqWebUI.Models.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
@* Value-only display for the port option. Edit mode: number input (unless readonly); View: text with "(default DNS port)". *@
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Descriptor.GetSource()?.IsReadOnly != true)
@@ -1,7 +1,7 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
@using DnsmasqWebUI.Models.Config
@using DnsmasqWebUI.Services.Abstractions
@using DnsmasqWebUI.Infrastructure.Services.Abstractions
@inject IEffectiveConfigRenderFragmentRegistry RenderFragmentRegistry
@* Custom fragment renders only the value; we keep the row shell (label + value slot + badge) here for consistency. *@
@@ -1,5 +1,5 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
<span class="ec-field-value-item" title="@(Source?.GetReadOnlyTooltip())">
@Value
@@ -1,5 +1,5 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
<li class="ec-field">
<span class="ec-field-label" title="@LabelTooltip"><strong>@Label:</strong></span>
@@ -1,5 +1,5 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
<li class="ec-field" title="@(Source?.GetReadOnlyTooltip())">
<span class="ec-field-label" title="@(LabelTooltip ?? Source?.GetReadOnlyTooltip())"><strong>@Label:</strong></span>
@@ -1,6 +1,5 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.Status
@using DnsmasqWebUI.Models.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
@if (Status != null)
{
@@ -1,5 +1,5 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
<CascadingValue Value="DisplayMode">
<div class="ec-section @(IsOpen ? "ec-section-open" : "")">
@@ -1,5 +1,5 @@
using DnsmasqWebUI.Client.Http.Abstractions;
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
using DnsmasqWebUI.Models.Dnsmasq;
using Microsoft.AspNetCore.Components;
namespace DnsmasqWebUI.Components.Dnsmasq;
@@ -1,5 +1,5 @@
using DnsmasqWebUI.Client.Http.Abstractions;
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
using DnsmasqWebUI.Models.Dnsmasq;
using Microsoft.AspNetCore.Components;
namespace DnsmasqWebUI.Components.Dnsmasq;
@@ -1,5 +1,5 @@
using DnsmasqWebUI.Client.Models;
using DnsmasqWebUI.Client.Services.Abstractions;
using DnsmasqWebUI.Models.Client;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
@@ -24,9 +24,12 @@ else if (_status != null)
<h2 class="page-section-title">Service configuration</h2>
<p class="mb-1"><strong>Config files (load order):</strong></p>
<ul class="list-unstyled small ms-2 mb-0">
@if (_configSet != null && _configSet.Files?.Count > 0)
@{
var configFileList = _status.ConfigFiles?.Count > 0 ? _status.ConfigFiles : _configSet?.Files;
}
@if (configFileList != null && configFileList.Count > 0)
{
@foreach (var f in _configSet.Files)
@foreach (var f in configFileList)
{
var isMain = string.Equals(f.Path, _status.MainConfigPath, StringComparison.Ordinal);
var isManaged = f.IsManaged;
+8 -7
View File
@@ -7,19 +7,20 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using DnsmasqWebUI
@using DnsmasqWebUI.Client.Models
@using DnsmasqWebUI.Client.Services.Abstractions
@using DnsmasqWebUI.Models.Client
@using DnsmasqWebUI.Infrastructure.Services.Abstractions
@using DnsmasqWebUI.Components
@using DnsmasqWebUI.Components.Modals
@using DnsmasqWebUI.Components.Dnsmasq
@using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Components.Hosts
@using DnsmasqWebUI.Infrastructure
@using DnsmasqWebUI.Infrastructure.Helpers.Config
@using DnsmasqWebUI.Models
@using DnsmasqWebUI.Models.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.Hosts
@using DnsmasqWebUI.Models.Dhcp
@using DnsmasqWebUI.Models.Status
@using DnsmasqWebUI.Configuration
@using DnsmasqWebUI.Client.Http.Abstractions
@using DnsmasqWebUI.Services.Abstractions
@using DnsmasqWebUI.Models.Config
@using DnsmasqWebUI.Infrastructure.Client.Abstractions
@using Microsoft.Extensions.Options
@@ -1,6 +1,8 @@
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace DnsmasqWebUI.Controllers;
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace DnsmasqWebUI.Controllers;
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Hosts;
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace DnsmasqWebUI.Controllers;
@@ -1,5 +1,5 @@
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace DnsmasqWebUI.Controllers;
@@ -1,4 +1,4 @@
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace DnsmasqWebUI.Controllers;
@@ -1,7 +1,8 @@
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Configuration;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@@ -76,6 +77,7 @@ public class StatusController : ControllerBase
ManagedFilePathExists: !string.IsNullOrEmpty(set.ManagedFilePath) && System.IO.File.Exists(set.ManagedFilePath),
LeasesPathConfigured: !string.IsNullOrEmpty(effectiveLeasesPath),
LeasesPathExists: !string.IsNullOrEmpty(effectiveLeasesPath) && System.IO.File.Exists(effectiveLeasesPath),
ConfigFiles: set.Files.Count > 0 ? set.Files : null,
ReloadCommandConfigured: !string.IsNullOrWhiteSpace(_options.ReloadCommand),
StatusCommandConfigured: !string.IsNullOrWhiteSpace(_options.StatusCommand),
StatusShowConfigured: !string.IsNullOrWhiteSpace(_options.StatusShowCommand),
@@ -1,7 +1,7 @@
using System.Reflection;
using DnsmasqWebUI.Client.Http;
using DnsmasqWebUI.Client.Http.Abstractions;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Client;
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Client.Http.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for GET api/config/set.</summary>
public interface IConfigSetClient
@@ -1,7 +1,8 @@
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Client.Http.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for GET/PUT api/dhcp/hosts.</summary>
public interface IDhcpHostsClient
@@ -1,7 +1,8 @@
using DnsmasqWebUI.Models.Hosts;
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Client.Http.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for GET/PUT api/hosts.</summary>
public interface IHostsClient
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Dhcp;
namespace DnsmasqWebUI.Client.Http.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for GET api/leases.</summary>
public interface ILeasesClient
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
namespace DnsmasqWebUI.Client.Http.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for POST api/reload.</summary>
public interface IReloadClient
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Models.Dnsmasq;
namespace DnsmasqWebUI.Client.Http.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// <summary>Typed client for GET api/status.</summary>
public interface IStatusClient
@@ -1,8 +1,10 @@
using DnsmasqWebUI.Client.Http.Abstractions;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
using DnsmasqWebUI.Infrastructure.Helpers.Http;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using System.Net.Http.Json;
namespace DnsmasqWebUI.Client.Http;
namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class ConfigSetClient : IConfigSetClient
{
@@ -11,6 +13,6 @@ public sealed class ConfigSetClient : IConfigSetClient
public ConfigSetClient(HttpClient http) => _http = http;
public async Task<DnsmasqConfigSet> GetConfigSetAsync(CancellationToken ct = default) =>
await _http.GetFromJsonAsync<DnsmasqConfigSet>("api/config/set", ct)
await _http.GetFromJsonAsync<DnsmasqConfigSet>("api/config/set", ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/config/set.");
}
@@ -1,10 +1,12 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using DnsmasqWebUI.Client.Http.Abstractions;
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
using DnsmasqWebUI.Infrastructure.Helpers.Http;
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Client.Http;
namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class DhcpHostsClient : IDhcpHostsClient
{
@@ -14,13 +16,13 @@ public sealed class DhcpHostsClient : IDhcpHostsClient
public async Task<IReadOnlyList<DhcpHostEntry>> GetDhcpHostsAsync(CancellationToken ct = default)
{
var list = await _http.GetFromJsonAsync<List<DhcpHostEntry>>("api/dhcp/hosts", ct);
var list = await _http.GetFromJsonAsync<List<DhcpHostEntry>>("api/dhcp/hosts", ApiJsonOptions.ClientOptions, ct);
return list ?? new List<DhcpHostEntry>();
}
public async Task<SaveWithReloadResult> SaveDhcpHostsAsync(IReadOnlyList<DhcpHostEntry> entries, CancellationToken ct = default)
{
var response = await _http.PutAsJsonAsync("api/dhcp/hosts", entries, ct);
var response = await _http.PutAsJsonAsync("api/dhcp/hosts", entries, ApiJsonOptions.ClientOptions, ct);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(ct);
@@ -29,7 +31,7 @@ public sealed class DhcpHostsClient : IDhcpHostsClient
{
try
{
var err = System.Text.Json.JsonSerializer.Deserialize<JsonError>(body);
var err = System.Text.Json.JsonSerializer.Deserialize<JsonError>(body, ApiJsonOptions.ClientOptions);
if (!string.IsNullOrEmpty(err?.Error))
msg = err.Error;
}
@@ -37,7 +39,7 @@ public sealed class DhcpHostsClient : IDhcpHostsClient
}
throw new HttpRequestException(msg);
}
return await response.Content.ReadFromJsonAsync<SaveWithReloadResult>(ct)
return await response.Content.ReadFromJsonAsync<SaveWithReloadResult>(ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/dhcp/hosts.");
}
@@ -1,9 +1,11 @@
using DnsmasqWebUI.Client.Http.Abstractions;
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
using DnsmasqWebUI.Infrastructure.Helpers.Http;
using DnsmasqWebUI.Models.Hosts;
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using System.Net.Http.Json;
namespace DnsmasqWebUI.Client.Http;
namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class HostsClient : IHostsClient
{
@@ -13,21 +15,21 @@ public sealed class HostsClient : IHostsClient
public async Task<IReadOnlyList<HostEntry>> GetHostsAsync(CancellationToken ct = default)
{
var list = await _http.GetFromJsonAsync<List<HostEntry>>("api/hosts", ct);
var list = await _http.GetFromJsonAsync<List<HostEntry>>("api/hosts", ApiJsonOptions.ClientOptions, ct);
return list ?? new List<HostEntry>();
}
public async Task<IReadOnlyList<ReadOnlyHostsFile>> GetReadOnlyHostsAsync(CancellationToken ct = default)
{
var list = await _http.GetFromJsonAsync<List<ReadOnlyHostsFile>>("api/hosts/readonly", ct);
var list = await _http.GetFromJsonAsync<List<ReadOnlyHostsFile>>("api/hosts/readonly", ApiJsonOptions.ClientOptions, ct);
return list ?? new List<ReadOnlyHostsFile>();
}
public async Task<SaveWithReloadResult> SaveHostsAsync(IReadOnlyList<HostEntry> entries, CancellationToken ct = default)
{
var response = await _http.PutAsJsonAsync("api/hosts", entries, ct);
var response = await _http.PutAsJsonAsync("api/hosts", entries, ApiJsonOptions.ClientOptions, ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SaveWithReloadResult>(ct)
return await response.Content.ReadFromJsonAsync<SaveWithReloadResult>(ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/hosts.");
}
}
@@ -1,8 +1,9 @@
using DnsmasqWebUI.Client.Http.Abstractions;
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
using DnsmasqWebUI.Infrastructure.Helpers.Http;
using DnsmasqWebUI.Models.Dhcp;
using System.Net.Http.Json;
namespace DnsmasqWebUI.Client.Http;
namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class LeasesClient : ILeasesClient
{
@@ -13,7 +14,7 @@ public sealed class LeasesClient : ILeasesClient
public async Task<LeasesResult> GetLeasesAsync(bool forceRefresh = false, CancellationToken ct = default)
{
var url = forceRefresh ? "api/leases?refresh=true" : "api/leases";
return await _http.GetFromJsonAsync<LeasesResult>(url, ct)
return await _http.GetFromJsonAsync<LeasesResult>(url, ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/leases.");
}
}
@@ -1,8 +1,9 @@
using DnsmasqWebUI.Client.Http.Abstractions;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
using DnsmasqWebUI.Infrastructure.Helpers.Http;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using System.Net.Http.Json;
namespace DnsmasqWebUI.Client.Http;
namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class ReloadClient : IReloadClient
{
@@ -14,7 +15,7 @@ public sealed class ReloadClient : IReloadClient
{
var response = await _http.PostAsync("api/reload", null, ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ReloadResult>(ct)
return await response.Content.ReadFromJsonAsync<ReloadResult>(ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/reload.");
}
}
@@ -1,7 +1,7 @@
using System.Net;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
namespace DnsmasqWebUI.Client.Http;
namespace DnsmasqWebUI.Infrastructure.Client;
/// <summary>
/// DelegatingHandler that rewrites relative request URIs to the current request's scheme, host, and path base.
@@ -0,0 +1,18 @@
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
using DnsmasqWebUI.Infrastructure.Helpers.Http;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using System.Net.Http.Json;
namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class StatusClient : IStatusClient
{
private readonly HttpClient _http;
public StatusClient(HttpClient http) => _http = http;
public async Task<DnsmasqServiceStatus> GetStatusAsync(CancellationToken ct = default) =>
await _http.GetFromJsonAsync<DnsmasqServiceStatus>("api/status", ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/status.");
}
@@ -1,9 +0,0 @@
using System.Text;
namespace DnsmasqWebUI.Infrastructure;
/// <summary>Encoding for dnsmasq config and hosts files. UTF-8 without BOM so dnsmasq does not see a BOM as "bad option" on line 1.</summary>
public static class DnsmasqFileEncoding
{
public static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
}
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Models.Config;
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
/// <summary>
/// Literal dnsmasq .conf option names (config keys). Use these everywhere we reference option names
@@ -0,0 +1,28 @@
using System.Text;
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
/// <summary>Encoding and reading helpers for dnsmasq config and hosts files. UTF-8 without BOM so dnsmasq does not see a BOM as "bad option" on line 1.</summary>
public static class DnsmasqFileEncoding
{
private const char Utf8BomChar = '\uFEFF';
public static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
/// <summary>Read config file as UTF-8 and strip UTF-8 BOM from first line if present, so conf-dir= etc. are recognized.</summary>
public static string[] ReadConfigLines(string path)
{
var lines = File.ReadAllLines(path, Encoding.UTF8);
StripBomFromFirstLine(lines);
return lines;
}
/// <summary>Strip UTF-8 BOM from first line if present (mutates list). Use so we do not perpetuate BOM when writing.</summary>
public static void StripBomFromFirstLine(IList<string> lines)
{
if (lines.Count == 0) return;
var first = lines[0];
if (first.Length > 0 && first[0] == Utf8BomChar)
lines[0] = first[1..];
}
}
@@ -1,6 +1,6 @@
using System.Collections.Frozen;
namespace DnsmasqWebUI.Models.Config;
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
/// <summary>
/// Short explainer tooltips for dnsmasq option names. Shown on hover over the field label.
@@ -1,7 +1,7 @@
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Models.EffectiveConfig;
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
/// <summary>
/// Builds field descriptors for viewing and editing. Mapping is done once here via delegates passed to each descriptor.
@@ -0,0 +1,26 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace DnsmasqWebUI.Infrastructure.Helpers.Http;
/// <summary>
/// Shared JSON options for API serialization (Controllers) and client deserialization (HTTP clients).
/// Keeps camelCase and enum-as-string in one place so server and client stay in sync.
/// </summary>
public static class ApiJsonOptions
{
/// <summary>Options for Controllers (AddJsonOptions). Apply via ConfigureServer(JsonSerializerOptions).</summary>
public static void ConfigureServer(JsonSerializerOptions options)
{
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.Converters.Add(new JsonStringEnumConverter());
}
/// <summary>Options for HTTP clients when deserializing API responses (camelCase, case-insensitive, enum as string).</summary>
public static JsonSerializerOptions ClientOptions { get; } = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
Converters = { new JsonStringEnumConverter() }
};
}
@@ -2,7 +2,7 @@ using Superpower;
using Superpower.Model;
using Superpower.Parsers;
namespace DnsmasqWebUI.Parsers;
namespace DnsmasqWebUI.Infrastructure.Parsers;
/// <summary>
/// Shared Superpower text-parser helpers used by .conf and related parsers.
@@ -3,7 +3,7 @@ using Superpower;
using Superpower.Model;
using Superpower.Parsers;
namespace DnsmasqWebUI.Parsers;
namespace DnsmasqWebUI.Infrastructure.Parsers;
/// <summary>
/// Parses a single <c>dhcp-host=</c> line from a dnsmasq .conf file. Same format as long option without <c>--</c>.
@@ -2,7 +2,7 @@ using Superpower;
using Superpower.Model;
using Superpower.Parsers;
namespace DnsmasqWebUI.Parsers;
namespace DnsmasqWebUI.Infrastructure.Parsers;
/// <summary>
/// Key/value parsing for dnsmasq .conf lines. Used by <see cref="DnsmasqConfIncludeParser"/> to build effective config.
@@ -1,10 +1,10 @@
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;
namespace DnsmasqWebUI.Parsers;
namespace DnsmasqWebUI.Infrastructure.Parsers;
/// <summary>
/// Parses a dnsmasq .conf file line-by-line into <see cref="DnsmasqConfLine"/> variants (BlankLine, CommentLine, DhcpHostLine, OtherLine).
@@ -1,10 +1,11 @@
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;
namespace DnsmasqWebUI.Parsers;
namespace DnsmasqWebUI.Infrastructure.Parsers;
/// <summary>
/// Parses the main dnsmasq .conf file only for <c>conf-file=</c> and <c>conf-dir=</c> directives
@@ -21,6 +22,7 @@ public static class DnsmasqConfIncludeParser
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).
@@ -71,7 +73,7 @@ public static class DnsmasqConfIncludeParser
if (!seenPaths.Add(canonicalPath))
yield break; // dnsmasq skips re-reading same file (by inode; we use path)
var lines = File.ReadAllLines(currentFilePath);
var lines = DnsmasqFileEncoding.ReadConfigLines(currentFilePath);
var dir = Path.GetDirectoryName(canonicalPath) ?? "";
// Yield "file started" so callers (e.g. GetIncludedPathsWithSource) can add this file even if it only has conf-file/conf-dir lines.
@@ -100,6 +102,7 @@ public static class DnsmasqConfIncludeParser
var (directory, matchSuffix, ignoreSuffix) = ParseConfDirValue(value);
if (string.IsNullOrEmpty(directory)) continue;
var resolvedDir = ResolvePath(directory, dir) ?? directory;
resolvedDir = Path.GetFullPath(resolvedDir);
if (!Directory.Exists(resolvedDir)) continue;
var files = GetConfDirFilesSorted(resolvedDir, matchSuffix, ignoreSuffix);
foreach (var f in files)
@@ -191,7 +194,7 @@ public static class DnsmasqConfIncludeParser
if (!File.Exists(mainFull))
return null;
foreach (var line in File.ReadAllLines(mainFull))
foreach (var line in DnsmasqFileEncoding.ReadConfigLines(mainFull))
{
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
if (kv == null)
@@ -3,7 +3,7 @@ using Superpower;
using Superpower.Model;
using Superpower.Parsers;
namespace DnsmasqWebUI.Parsers;
namespace DnsmasqWebUI.Infrastructure.Parsers;
/// <summary>
/// Parses a single line of the dnsmasq DHCPv4 lease file (e.g. /var/lib/misc/dnsmasq.leases).
@@ -3,7 +3,7 @@ using Superpower;
using Superpower.Model;
using Superpower.Parsers;
namespace DnsmasqWebUI.Parsers;
namespace DnsmasqWebUI.Infrastructure.Parsers;
/// <summary>
/// Parses a single line of an /etc/hosts-style file. Format: IP_address canonical_hostname [aliases...]
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Hosting;
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>
/// Marker interface for application hosted services that are registered via assembly scanning.
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>
/// Marker for types that are registered as their own concrete type with scoped lifetime.
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>
/// Marker interface for application services that are registered as scoped.
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>
/// Marker interface for application services that are registered as singleton.
@@ -1,7 +1,6 @@
using DnsmasqWebUI.Client.Models;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Client;
namespace DnsmasqWebUI.Client.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>
/// Loads and saves client-side preferences (e.g. polling intervals) to browser local storage.
@@ -1,4 +1,6 @@
namespace DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Contracts;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>
/// Singleton cache for the config set (main + conf-dir + managed file). Invalidates on file watchers, staleness, or manual Invalidate.
@@ -1,8 +1,10 @@
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
public interface IDnsmasqConfigService : IApplicationScopedService
{
@@ -11,6 +13,3 @@ public interface IDnsmasqConfigService : IApplicationScopedService
Task<ManagedConfigContent> ReadManagedConfigAsync(CancellationToken ct = default);
Task WriteManagedConfigAsync(IReadOnlyList<DnsmasqConfLine> lines, CancellationToken ct = default);
}
/// <summary>Full managed file content. EffectiveHostsPathInFile is parsed from AddnHosts line if present (for display only).</summary>
public record ManagedConfigContent(IReadOnlyList<DnsmasqConfLine> Lines, string EffectiveHostsPathInFile);
@@ -1,6 +1,7 @@
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>Discovers the dnsmasq config set (main + conf-file/conf-dir) and the managed file path.</summary>
public interface IDnsmasqConfigSetService : IApplicationSingleton
@@ -1,7 +1,8 @@
using Microsoft.AspNetCore.Components;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>
/// Lookup for custom display fragments per effective-config field (section + option).
@@ -1,6 +1,7 @@
using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Models.Hosts;
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>
/// Singleton cache for hosts: managed hosts file + read-only (system + addn-hosts). Invalidates on watchers, staleness, or Invalidate.
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Hosts;
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
public interface IHostsFileService : IApplicationScopedService
{
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Dhcp;
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>
/// Singleton cache for the leases file; invalidates when the file changes (e.g. via file watcher).
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Dhcp;
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
public interface ILeasesFileService : IApplicationScopedService
{
@@ -1,4 +1,6 @@
namespace DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Contracts;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// <summary>Runs a shell command with timeout and cancellation. Used for status, reload, status show, and logs commands.</summary>
public interface IProcessRunner : IApplicationScopedService
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
public interface IReloadService : IApplicationScopedService
{
@@ -1,9 +1,9 @@
using System.Text.Json;
using DnsmasqWebUI.Client.Models;
using DnsmasqWebUI.Client.Services.Abstractions;
using DnsmasqWebUI.Models.Client;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.JSInterop;
namespace DnsmasqWebUI.Client.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
/// <summary>
/// Loads and saves client-side settings to browser local storage via JS interop.
@@ -1,13 +1,14 @@
using System.Text;
using DnsmasqWebUI.Configuration;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using DnsmasqWebUI.Infrastructure.Parsers;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.Options;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
/// <summary>
/// Singleton cache for the config set (main + conf-dir + managed file). Reads all files once per refresh,
@@ -203,7 +204,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
}
try
{
dict[canonical] = File.ReadAllLines(path, Encoding.UTF8);
dict[canonical] = DnsmasqFileEncoding.ReadConfigLines(path);
}
catch
{
@@ -1,12 +1,14 @@
using System.Text;
using DnsmasqWebUI.Infrastructure;
using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using DnsmasqWebUI.Infrastructure.Parsers;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
public class DnsmasqConfigService : IDnsmasqConfigService
{
@@ -113,7 +115,9 @@ public class DnsmasqConfigService : IDnsmasqConfigService
if (File.Exists(path))
{
var read = await File.ReadAllLinesAsync(path, DnsmasqFileEncoding.Utf8NoBom, ct);
rawLines = StripBomFromFirstLine(read);
var list = read.ToList();
DnsmasqFileEncoding.StripBomFromFirstLine(list);
rawLines = list;
}
else
rawLines = Array.Empty<string>();
@@ -203,17 +207,4 @@ public class DnsmasqConfigService : IDnsmasqConfigService
_logger.LogInformation("Wrote managed config file: {Path}", path);
}
/// <summary>Strip UTF-8 BOM from first line if present.</summary>
private static IReadOnlyList<string> StripBomFromFirstLine(IReadOnlyList<string> lines)
{
if (lines.Count == 0) return lines;
var first = lines[0];
if (first.Length > 0 && first[0] == '\uFEFF')
{
var list = lines.ToList();
list[0] = first[1..];
return list;
}
return lines;
}
}
@@ -1,7 +1,9 @@
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
public class DnsmasqConfigSetService : IDnsmasqConfigSetService
{
@@ -1,10 +1,11 @@
using Microsoft.AspNetCore.Components;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
/// <summary>
/// Maps (sectionId, optionName) to custom effective-config value display component types and builds
@@ -1,13 +1,12 @@
using System.Text;
using DnsmasqWebUI.Infrastructure;
using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Configuration;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Infrastructure.Parsers;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.Options;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
/// <summary>
/// Runs once at startup: ensures the managed file is included only by a conf-file= directive (never conf-dir=),
@@ -52,7 +51,7 @@ public class EnsureManagedConfigHostedService : IApplicationHostedService
var lines = File.Exists(mainFull)
? (await File.ReadAllLinesAsync(mainFull, DnsmasqFileEncoding.Utf8NoBom, cancellationToken)).ToList()
: new List<string>();
StripBomFromFirstLine(lines);
DnsmasqFileEncoding.StripBomFromFirstLine(lines);
// Remove any conf-file= line that points to our managed path (we will add it as the last line).
for (var i = lines.Count - 1; i >= 0; i--)
@@ -96,14 +95,5 @@ public class EnsureManagedConfigHostedService : IApplicationHostedService
await configService.WriteManagedConfigAsync(Array.Empty<DnsmasqConfLine>(), cancellationToken);
}
/// <summary>Strip UTF-8 BOM from first line if present, so we do not perpetuate it when writing.</summary>
private static void StripBomFromFirstLine(List<string> lines)
{
if (lines.Count == 0) return;
var first = lines[0];
if (first.Length > 0 && first[0] == '\uFEFF')
lines[0] = first[1..];
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
@@ -1,11 +1,12 @@
using System.Text;
using DnsmasqWebUI.Configuration;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Models.Hosts;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Parsers;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.Options;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
/// <summary>
/// Singleton cache for hosts: managed hosts file + read-only (system + addn-hosts). Invalidates on watchers (managed + system path), staleness, or Invalidate.
@@ -1,9 +1,9 @@
using DnsmasqWebUI.Infrastructure;
using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Hosts;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Parsers;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
public class HostsFileService : IHostsFileService
{
@@ -1,9 +1,9 @@
using System.Text;
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Parsers;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
/// <summary>
/// Singleton cache for the leases file with a <see cref="FileSystemWatcher"/> so we re-read only when the file changes.
@@ -1,7 +1,7 @@
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
public class LeasesFileService : ILeasesFileService
{
@@ -1,9 +1,10 @@
using System.Diagnostics;
using System.Text;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.Logging;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
/// <summary>Runs shell commands via /bin/sh with async output capture and timeout. Used by StatusController and ReloadService.</summary>
public sealed class ProcessRunner : IProcessRunner
@@ -1,8 +1,8 @@
using DnsmasqWebUI.Configuration;
using DnsmasqWebUI.Services.Abstractions;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.Options;
namespace DnsmasqWebUI.Services;
namespace DnsmasqWebUI.Infrastructure.Services;
public class ReloadService : IReloadService
{
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Client.Models;
namespace DnsmasqWebUI.Models.Client;
/// <summary>
/// Client-side preferences persisted in browser local storage.
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Client.Models;
namespace DnsmasqWebUI.Models.Client;
/// <summary>
/// Which subset of client settings to show in the settings modal.
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Configuration;
namespace DnsmasqWebUI.Models.Config;
/// <summary>
/// Application-level settings (title, etc.). Bound from "Application" config section
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Configuration;
namespace DnsmasqWebUI.Models.Config;
/// <summary>
/// Configuration for dnsmasq paths and reload/status commands.
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Options;
namespace DnsmasqWebUI.Configuration;
namespace DnsmasqWebUI.Models.Config;
/// <summary>
/// Validates required dnsmasq options at startup. If config is missing or default paths don't point at existing files,
@@ -1,7 +1,8 @@
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Models.Dnsmasq;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Services.Abstractions;
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>
public record ConfigSetSnapshot(
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Hosts;
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Models.Contracts;
/// <summary>Immutable snapshot from the hosts cache: managed hosts file entries and read-only hosts files (system + addn-hosts).</summary>
public record HostsSnapshot(
@@ -0,0 +1,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>
public record ManagedConfigContent(IReadOnlyList<DnsmasqConfLine> Lines, string EffectiveHostsPathInFile);
@@ -1,6 +1,6 @@
namespace DnsmasqWebUI.Services.Abstractions;
namespace DnsmasqWebUI.Models.Contracts;
/// <summary>Result of running a shell command via <see cref="IProcessRunner"/>.</summary>
/// <summary>Result of running a shell command via IProcessRunner.</summary>
/// <param name="ExitCode">Process exit code, or null if timed out or failed to start.</param>
/// <param name="Stdout">Standard output.</param>
/// <param name="Stderr">Standard error.</param>
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
namespace DnsmasqWebUI.Models.Status;
namespace DnsmasqWebUI.Models.Dnsmasq;
/// <summary>
/// Dnsmasq service and config status returned by GET api/status.
@@ -21,6 +21,7 @@ namespace DnsmasqWebUI.Models.Status;
/// <param name="ManagedFilePathExists">True if <paramref name="ManagedFilePath"/> exists on disk.</param>
/// <param name="LeasesPathConfigured">True when dhcp-leasefile is set in effective config.</param>
/// <param name="LeasesPathExists">True if <paramref name="LeasesPath"/> is set and the file exists on disk.</param>
/// <param name="ConfigFiles">Ordered list of config files (main + conf-file + conf-dir) for "Config files (load order)" in the UI. Null or empty when config set is unavailable.</param>
/// <param name="ReloadCommandConfigured">True when ReloadCommand is set in app config (reload after save).</param>
/// <param name="StatusCommandConfigured">True when StatusCommand is set (used to determine dnsmasq active/inactive).</param>
/// <param name="StatusShowConfigured">True when StatusShowCommand is set (full service status output on Dnsmasq page).</param>
@@ -52,6 +53,7 @@ public record DnsmasqServiceStatus(
bool ManagedFilePathExists,
bool LeasesPathConfigured,
bool LeasesPathExists,
IReadOnlyList<DnsmasqConfigSetEntry>? ConfigFiles,
bool ReloadCommandConfigured,
bool StatusCommandConfigured,
bool StatusShowConfigured,
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Models.EffectiveConfig;
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
/// <summary>
/// Where an effective config value came from (which file and line). Used so the UI can show exactly which file
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Models.EffectiveConfig;
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
/// <summary>Source of a file in the dnsmasq config set: main config, conf-file=, or conf-dir=.</summary>
public enum DnsmasqConfFileSource
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Models.EffectiveConfig;
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>
public record DnsmasqConfigSet(
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Models.EffectiveConfig;
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>
public record DnsmasqConfigSetEntry(string Path, string FileName, DnsmasqConfFileSource Source, bool IsManaged);
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Models.EffectiveConfig;
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
/// <summary>
/// Display mode for effective-config fields. Cascaded from the parent so that
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Status;
using DnsmasqWebUI.Models.Dnsmasq;
namespace DnsmasqWebUI.Models.EffectiveConfig;
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
/// <summary>
/// Glue for rendering one effective-config field: metadata (section, option name) plus delegates that resolve value, source, and items from Status at render time.
@@ -1,4 +1,4 @@
namespace DnsmasqWebUI.Models.EffectiveConfig;
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
/// <summary>
/// Source file for each field in <see cref="EffectiveDnsmasqConfig"/>.

Some files were not shown because too many files have changed in this diff Show More