Deprecate mDNS scanning (Closes #1454) (#1455)

This commit is contained in:
2026-02-19 08:08:09 -08:00
parent 77c2403a40
commit 4077777cd9
22 changed files with 132 additions and 402 deletions
@@ -1,9 +1,9 @@
using Daybreak.Extensions;
using Daybreak.Linux.Services.Api;
using Daybreak.Linux.Services.ApplicationLauncher;
using Daybreak.Linux.Services.Credentials;
using Daybreak.Linux.Services.Injection;
using Daybreak.Linux.Services.Keyboard;
using Daybreak.Linux.Services.MDns;
using Daybreak.Linux.Services.Privilege;
using Daybreak.Linux.Services.Screens;
using Daybreak.Linux.Services.Startup.Actions;
@@ -27,7 +27,7 @@ using Daybreak.Shared.Services.FileProviders;
using Daybreak.Shared.Services.Initialization;
using Daybreak.Shared.Services.Injection;
using Daybreak.Shared.Services.Keyboard;
using Daybreak.Shared.Services.MDns;
using Daybreak.Shared.Services.Api;
using Daybreak.Shared.Services.Privilege;
using Daybreak.Shared.Services.Screens;
using Daybreak.Shared.Services.Themes;
@@ -60,7 +60,7 @@ public sealed class LinuxPlatformConfiguration : PluginConfigurationBase
services.AddScoped<IRegistryService, RegistryService>();
services.AddSingleton<ISystemThemeDetector, SystemThemeDetector>();
services.AddSingleton<ISevenZipExtractor, SevenZipArchiveExtractor>();
services.AddHostedSingleton<IMDomainRegistrar, PortScanningDomainRegistrar>();
services.AddSingleton<IPidProvider, PidProvider>();
services.AddSingleton<IWindowManipulationService, WindowManipulationService>();
services.AddSingleton<ICrashDumpService, CrashDumpService>();
services.AddSingleton<IDaybreakRestartingService, DaybreakRestartingService>();
@@ -0,0 +1,22 @@
using Daybreak.Linux.Services.Wine;
using Daybreak.Shared.Services.Api;
namespace Daybreak.Linux.Services.Api;
/// <summary>
/// Linux implementation of <see cref="IPidProvider"/>.
/// On Linux, the reported PID is a Wine-internal PID that must be
/// converted to the actual Linux system PID via <see cref="IWinePidMapper"/>.
/// </summary>
public sealed class PidProvider(IWinePidMapper winePidMapper) : IPidProvider
{
private readonly IWinePidMapper winePidMapper = winePidMapper;
/// <inheritdoc />
public int ResolveSystemPid(int reportedPid, string executableName)
{
// On Linux, the reported PID is a Wine PID. Convert to Linux system PID.
var linuxPid = this.winePidMapper.WinePidToLinuxPid(reportedPid, executableName);
return linuxPid ?? reportedPid;
}
}
@@ -121,19 +121,6 @@ public sealed class GuildWarsProcessFinder(
continue;
}
// Verify this process belongs to our Wine prefix
var environPath = Path.Combine(procDir, "environ");
if (!File.Exists(environPath))
{
continue;
}
var environ = File.ReadAllText(environPath);
if (!environ.Contains(prefixPath, StringComparison.Ordinal))
{
continue;
}
// Extract the executable path from cmdline
// cmdline is null-separated; find the segment containing Gw.exe
var segments = cmdline.Split('\0', StringSplitOptions.RemoveEmptyEntries);
@@ -1,184 +0,0 @@
using System.Net.Http.Json;
using System.Text.Json;
using Daybreak.Linux.Services.Wine;
using Daybreak.Shared.Models.Api;
using Daybreak.Shared.Services.MDns;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Extensions.Core;
namespace Daybreak.Linux.Services.MDns;
/// <summary>
/// Linux-specific implementation of <see cref="IMDomainRegistrar"/>.
/// Since mDNS announcements from Wine don't propagate to the host,
/// this implementation scans a known port range (50805100) for running
/// Daybreak API instances and matches them to Guild Wars processes
/// by querying the API's health endpoint for its Wine PID, then
/// converting it to a Linux PID via <see cref="IWinePidMapper"/>.
/// All probes fire in parallel with a 500ms total timeout.
/// </summary>
public sealed class PortScanningDomainRegistrar(
IWinePidMapper winePidMapper,
ILogger<PortScanningDomainRegistrar> logger)
: IMDomainRegistrar, IHostedService, IDisposable
{
private const int StartPort = 5080;
private const int EndPort = 5100;
private const string HealthPath = "/api/v1/rest/health";
private const string DaybreakApiServicePrefix = "daybreak-api-";
private static readonly TimeSpan ProbeTimeout = TimeSpan.FromMilliseconds(500);
private static readonly TimeSpan ScanInterval = TimeSpan.FromSeconds(15);
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
private readonly IWinePidMapper winePidMapper = winePidMapper;
private readonly ILogger<PortScanningDomainRegistrar> logger = logger;
private readonly HttpClient httpClient = new() { Timeout = ProbeTimeout };
// Rebuilt on each scan — survives Daybreak restarts since it's based on live port probing.
private volatile Dictionary<string, List<Uri>> discoveredServices = [];
private CancellationTokenSource? backgroundCts;
Task IHostedService.StartAsync(CancellationToken cancellationToken)
{
this.backgroundCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_ = Task.Factory.StartNew(
() => this.ScanPeriodically(this.backgroundCts.Token),
this.backgroundCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Current);
return Task.CompletedTask;
}
Task IHostedService.StopAsync(CancellationToken cancellationToken)
{
this.backgroundCts?.Cancel();
return Task.CompletedTask;
}
public void Dispose()
{
this.backgroundCts?.Dispose();
this.httpClient.Dispose();
}
public IReadOnlyList<Uri>? Resolve(string service)
{
if (this.discoveredServices.TryGetValue(service, out var uris) && uris.Count > 0)
{
return uris;
}
return default;
}
public IReadOnlyList<Uri>? QueryByServiceName(Func<string, bool> query)
{
var results = new List<Uri>();
foreach (var (serviceName, uris) in this.discoveredServices)
{
if (query(serviceName))
{
results.AddRange(uris);
}
}
return results.Count > 0 ? results : default;
}
public void QueryAllServices()
{
_ = Task.Run(() => this.ScanPortsAsync(CancellationToken.None));
}
private async Task ScanPeriodically(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
await this.ScanPortsAsync(cancellationToken);
await Task.Delay(ScanInterval, cancellationToken);
}
}
private async Task ScanPortsAsync(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var newServices = new Dictionary<string, List<Uri>>();
using var timeoutCts = new CancellationTokenSource(ProbeTimeout);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
var probeTasks = new List<Task<(int Port, int? ProcessId)>>();
for (var port = StartPort; port <= EndPort; port++)
{
probeTasks.Add(this.ProbePortAsync(port, linkedCts.Token));
}
try
{
await Task.WhenAll(probeTasks);
}
catch (OperationCanceledException)
{
}
foreach (var task in probeTasks)
{
if (!task.IsCompletedSuccessfully)
{
continue;
}
var (port, processId) = task.Result;
if (processId is null)
{
continue;
}
// The API reports its Wine PID. Convert to Linux PID.
var linuxPid = this.winePidMapper.WinePidToLinuxPid(processId.Value, "Gw.exe");
var effectivePid = linuxPid ?? processId.Value;
var serviceName = $"{DaybreakApiServicePrefix}{effectivePid}";
var uri = new Uri($"http://localhost:{port}");
scopedLogger.LogDebug(
"Found Daybreak API on port {Port} with Wine PID {WinePid} (Linux PID {LinuxPid})",
port,
processId.Value,
effectivePid);
if (!newServices.TryGetValue(serviceName, out var uriList))
{
uriList = [];
newServices[serviceName] = uriList;
}
uriList.Add(uri);
}
this.discoveredServices = newServices;
scopedLogger.LogDebug("Port scan complete. Found {Count} Daybreak API instance(s)", newServices.Count);
}
private async Task<(int Port, int? ProcessId)> ProbePortAsync(int port, CancellationToken cancellationToken)
{
try
{
var url = $"http://localhost:{port}{HealthPath}";
using var response = await this.httpClient.GetAsync(url, cancellationToken);
if (!response.IsSuccessStatusCode)
{
return (port, null);
}
var processIdResponse = await response.Content.ReadFromJsonAsync<ProcessIdResponse>(JsonOptions, cancellationToken);
return (port, processIdResponse?.ProcessId);
}
catch
{
return (port, null);
}
}
}
@@ -19,8 +19,6 @@ public sealed class WinePidMapper(
/// <inheritdoc />
public int? WinePidToLinuxPid(int winePid, string executableName)
{
var prefixPath = this.winePrefixManager.GetWinePrefixPath();
try
{
foreach (var procDir in Directory.EnumerateDirectories("/proc"))
@@ -45,19 +43,6 @@ public sealed class WinePidMapper(
continue;
}
// Verify this process belongs to our Wine prefix
var environPath = Path.Combine(procDir, "environ");
if (!File.Exists(environPath))
{
continue;
}
var environ = File.ReadAllText(environPath);
if (!environ.Contains(prefixPath, StringComparison.Ordinal))
{
continue;
}
this.logger.LogDebug(
"Resolved Wine PID {WinePid} -> Linux PID {LinuxPid} for {ExeName}",
winePid, pid, executableName