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,56 +0,0 @@
using Daybreak.Shared.Models;
using Daybreak.Shared.Services.MDns;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Extensions.Core;
namespace Daybreak.API.Services;
public sealed class ApiAdvertisingService(
IServer server,
IMDomainNameService mDnsService,
IHostApplicationLifetime lifetime,
ILogger<ApiAdvertisingService> logger) : IHostedService
{
private const string ProcessIdPlaceholder = "{PID}";
private const string ServiceName = $"daybreak-api-{ProcessIdPlaceholder}";
private const string ServiceSubType = "daybreak-api";
private readonly IServerAddressesFeature serverAddressesFeature = server.Features.Get<IServerAddressesFeature>() ?? throw new InvalidOperationException("Server does not support addresses feature.");
private readonly IMDomainNameService mDnsService = mDnsService;
private readonly IHostApplicationLifetime lifetime = lifetime;
private readonly ILogger<ApiAdvertisingService> logger = logger;
private DnsRegistrationToken? dnsToken;
public Task StartAsync(CancellationToken cancellationToken)
{
this.lifetime.ApplicationStarted.Register(this.RegisterDomain);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
this.dnsToken?.Dispose();
return Task.CompletedTask;
}
private void RegisterDomain()
{
var scopedLogger = this.logger.CreateScopedLogger();
var addresses = this.serverAddressesFeature?.Addresses.ToArray() ?? [];
var port = addresses
.Select(a => new Uri(a).Port)
.Distinct()
.FirstOrDefault();
scopedLogger.LogInformation("Found port {port}", port);
if (port is 0)
{
throw new InvalidOperationException("Unable to start advertising service. No port found");
}
var dnsEntry = ServiceName.Replace(ProcessIdPlaceholder, Environment.ProcessId.ToString());
scopedLogger.LogInformation("Advertising service {service}:{port}", dnsEntry, port);
this.dnsToken = this.mDnsService.RegisterDomain(dnsEntry, (ushort)port, ServiceSubType);
}
}
@@ -1,6 +1,5 @@
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Services.BuildTemplates;
using Daybreak.Shared.Services.MDns;
using System.Diagnostics.CodeAnalysis;
namespace Daybreak.API.Services;
@@ -11,8 +10,6 @@ public static class WebApplicationBuilderExtensions
{
builder.Services.AddSingleton<HashingService>();
builder.Services.AddSingleton<IBuildTemplateManager, BuildTemplateManager>();
builder.Services.AddSingleton<IMDomainNameService, MDomainNameService>();
builder.Services.AddHostedService<ApiAdvertisingService>();
builder.Services.AddSingleton<MemoryScanningService>();
builder.Services.AddSingleton<ChatService>();
builder.Services.AddSingleton<MainPlayerService>();
@@ -67,7 +67,6 @@ using Daybreak.Shared.Services.Initialization;
using Daybreak.Shared.Services.Injection;
using Daybreak.Shared.Services.InternetChecker;
using Daybreak.Shared.Services.LaunchConfigurations;
using Daybreak.Shared.Services.MDns;
using Daybreak.Shared.Services.Menu;
using Daybreak.Shared.Services.Metrics;
using Daybreak.Shared.Services.Mods;
@@ -159,7 +158,6 @@ public class ProjectConfiguration : PluginConfigurationBase
services.AddSingleton<INotificationStorage, InMemoryNotificationStorage>();
services.AddSingleton<IModsManager, ModsManager>();
services.AddSingleton<IPluginsService, PluginsService>();
services.AddSingleton<IMDomainNameService, MDomainNameService>();
services.AddSingleton<IAttachedApiAccessor, AttachedApiAccessor>();
services.AddSingleton<PrivilegeContext>();
services.AddSingleton<ViewRedirectContext>();
@@ -255,6 +253,7 @@ public class ProjectConfiguration : PluginConfigurationBase
services.AddHostedSingleton<ITradeAlertingService, TradeAlertingService>();
services.AddHostedSingleton<IGuildWarsExecutableManager, GuildWarsExecutableManager>();
services.AddHostedSingleton<IEventNotifierService, EventNotifierService>();
services.AddHostedSingleton<IApiScanningService, ApiScanningService>();
services.AddHostedSingleton<GameScreenshotsTheme>();
services.AddHostedService<StartupActionManager>();
services.AddHostedService<ProcessorUsageMonitor>();
-1
View File
@@ -40,7 +40,6 @@
<ItemGroup>
<PackageReference Include="Elastic.OpenTelemetry" />
<PackageReference Include="ksemenenko.ColorThief" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" />
@@ -1,47 +1,42 @@
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 Daybreak.Shared.Services.Api;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Extensions.Core;
namespace Daybreak.Linux.Services.MDns;
namespace Daybreak.Services.Api;
/// <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"/>.
/// 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.
/// All probes fire in parallel with a 500ms total timeout.
/// </summary>
public sealed class PortScanningDomainRegistrar(
IWinePidMapper winePidMapper,
ILogger<PortScanningDomainRegistrar> logger)
: IMDomainRegistrar, IHostedService, IDisposable
public sealed class ApiScanningService(
IPidProvider pidProvider,
ILogger<ApiScanningService> logger)
: IApiScanningService, 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 const string GuildWarsExecutable = "Gw.exe";
private static readonly TimeSpan ProbeTimeout = TimeSpan.FromMilliseconds(2000);
private static readonly TimeSpan ScanInterval = TimeSpan.FromSeconds(10);
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
private readonly IWinePidMapper winePidMapper = winePidMapper;
private readonly ILogger<PortScanningDomainRegistrar> logger = logger;
private readonly IPidProvider pidProvider = pidProvider;
private readonly ILogger<ApiScanningService> 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 volatile Dictionary<int, Uri> discoveredApis = [];
private CancellationTokenSource? backgroundCts;
Task IHostedService.StartAsync(CancellationToken cancellationToken)
{
this.backgroundCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
this.backgroundCts = new CancellationTokenSource();
_ = Task.Factory.StartNew(
() => this.ScanPeriodically(this.backgroundCts.Token),
this.backgroundCts.Token,
@@ -62,31 +57,31 @@ public sealed class PortScanningDomainRegistrar(
this.httpClient.Dispose();
}
public IReadOnlyList<Uri>? Resolve(string service)
public Uri? GetApiUriByProcessId(int processId)
{
if (this.discoveredServices.TryGetValue(service, out var uris) && uris.Count > 0)
if (this.discoveredApis.TryGetValue(processId, out var uri))
{
return uris;
return uri;
}
return default;
}
public IReadOnlyList<Uri>? QueryByServiceName(Func<string, bool> query)
public IReadOnlyList<(int ProcessId, Uri Uri)>? QueryByProcessId(Func<int, bool> predicate)
{
var results = new List<Uri>();
foreach (var (serviceName, uris) in this.discoveredServices)
var results = new List<(int ProcessId, Uri Uri)>();
foreach (var (processId, uri) in this.discoveredApis)
{
if (query(serviceName))
if (predicate(processId))
{
results.AddRange(uris);
results.Add((processId, uri));
}
}
return results.Count > 0 ? results : default;
}
public void QueryAllServices()
public void RequestScan()
{
_ = Task.Run(() => this.ScanPortsAsync(CancellationToken.None));
}
@@ -103,7 +98,7 @@ public sealed class PortScanningDomainRegistrar(
private async Task ScanPortsAsync(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var newServices = new Dictionary<string, List<Uri>>();
var newApis = new Dictionary<int, Uri>();
using var timeoutCts = new CancellationTokenSource(ProbeTimeout);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
@@ -129,36 +124,27 @@ public sealed class PortScanningDomainRegistrar(
continue;
}
var (port, processId) = task.Result;
if (processId is null)
var (port, reportedPid) = task.Result;
if (reportedPid 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}";
// Convert the reported PID (Wine PID on Linux) to system PID
var systemPid = this.pidProvider.ResolveSystemPid(reportedPid.Value, GuildWarsExecutable);
var uri = new Uri($"http://localhost:{port}");
scopedLogger.LogDebug(
"Found Daybreak API on port {Port} with Wine PID {WinePid} (Linux PID {LinuxPid})",
"Found Daybreak API on port {Port} with reported PID {ReportedPid} (system PID {SystemPid})",
port,
processId.Value,
effectivePid);
reportedPid.Value,
systemPid);
if (!newServices.TryGetValue(serviceName, out var uriList))
{
uriList = [];
newServices[serviceName] = uriList;
}
uriList.Add(uri);
newApis[systemPid] = uri;
}
this.discoveredServices = newServices;
scopedLogger.LogDebug("Port scan complete. Found {Count} Daybreak API instance(s)", newServices.Count);
this.discoveredApis = newApis;
scopedLogger.LogDebug("Port scan complete. Found {Count} Daybreak API instance(s)", newApis.Count);
}
private async Task<(int Port, int? ProcessId)> ProbePortAsync(int port, CancellationToken cancellationToken)
@@ -5,7 +5,6 @@ using Daybreak.Shared.Models.LaunchConfigurations;
using Daybreak.Shared.Models.Mods;
using Daybreak.Shared.Services.Api;
using Daybreak.Shared.Services.Injection;
using Daybreak.Shared.Services.MDns;
using Daybreak.Shared.Services.Notifications;
using Daybreak.Shared.Services.Options;
using Daybreak.Shared.Utils;
@@ -20,7 +19,7 @@ namespace Daybreak.Services.Api;
public sealed class DaybreakApiService(
IOptionsProvider optionsProvider,
IAttachedApiAccessor attachedApiAccessor,
IMDomainRegistrar mDomainRegistrar,
IApiScanningService apiScanningService,
IStubInjector stubInjector,
INotificationService notificationService,
IHttpClient<ScopedApiContext> scopedApiClient,
@@ -32,13 +31,10 @@ public sealed class DaybreakApiService(
private const string EntryPoint = "ThreadInit";
private const string LocalHost = "localhost";
private const string DaybreakApiName = "Api/Daybreak.API.dll";
private const string ProcessIdPlaceholder = "{PID}";
private const string DaybreakApiServiceName = $"daybreak-api-{ProcessIdPlaceholder}";
private const string ServiceSubType = "daybreak-api";
private readonly IOptionsProvider optionsProvider = optionsProvider.ThrowIfNull();
private readonly IAttachedApiAccessor attachedApiAccessor = attachedApiAccessor.ThrowIfNull();
private readonly IMDomainRegistrar mDomainRegistrar = mDomainRegistrar.ThrowIfNull();
private readonly IApiScanningService apiScanningService = apiScanningService.ThrowIfNull();
private readonly IStubInjector stubInjector = stubInjector.ThrowIfNull();
private readonly INotificationService notificationService = notificationService.ThrowIfNull();
private readonly IHttpClient<ScopedApiContext> scopedApiClient = scopedApiClient.ThrowIfNull();
@@ -124,12 +120,10 @@ public sealed class DaybreakApiService(
return default;
}
var serviceName = DaybreakApiServiceName.Replace(ProcessIdPlaceholder, guildWarsProcess.Id.ToString());
var serviceUris = this.mDomainRegistrar.Resolve(serviceName);
var serviceUri = serviceUris?.Count > 0 ? serviceUris[0] : default;
var serviceUri = this.apiScanningService.GetApiUriByProcessId(guildWarsProcess.Id);
if (serviceUri is null)
{
scopedLogger.LogWarning("Failed to find Daybreak API service by name {serviceName}", serviceName);
scopedLogger.LogWarning("Failed to find Daybreak API service for process {ProcessId}", guildWarsProcess.Id);
return default;
}
@@ -150,8 +144,8 @@ public sealed class DaybreakApiService(
public async Task<ScopedApiContext?> FindDaybreakApiContextByCredentials(LoginCredentials loginCredentials, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var serviceUris = this.mDomainRegistrar.QueryByServiceName(n => n.StartsWith(ServiceSubType));
foreach(var uri in serviceUris ?? [])
var discoveredApis = this.apiScanningService.QueryByProcessId(_ => true);
foreach (var (_, uri) in discoveredApis ?? [])
{
var uriBuilder = new UriBuilder(uri)
{
@@ -173,7 +167,7 @@ public sealed class DaybreakApiService(
public void RequestInstancesAnnouncement()
{
this.mDomainRegistrar.QueryAllServices();
this.apiScanningService.RequestScan();
}
public Task OnGuildWarsCreated(GuildWarsCreatedContext guildWarsCreatedContext, CancellationToken cancellationToken) =>
@@ -1,180 +0,0 @@
using Daybreak.Shared.Services.MDns;
using MeaMod.DNS.Model;
using MeaMod.DNS.Multicast;
using System.Collections.Concurrent;
using System.Net;
using Microsoft.Extensions.Logging;
using System.Core.Extensions;
using System.Extensions;
using System.Extensions.Core;
using Microsoft.Extensions.Hosting;
namespace Daybreak.Services.MDns;
public sealed class MDomainRegistrar(
ILogger<MDomainRegistrar> logger)
: IMDomainRegistrar, IHostedService
{
private static readonly TimeSpan ScanFrequency = TimeSpan.FromSeconds(15);
private static readonly TimeSpan MaxTTL = TimeSpan.FromSeconds(20);
private readonly ConcurrentDictionary<string, ServiceRegistration> serviceLookup = [];
private readonly ServiceDiscovery serviceDiscovery = new();
private readonly ILogger<MDomainRegistrar> logger = logger.ThrowIfNull();
Task IHostedService.StartAsync(CancellationToken cancellationToken)
{
this.serviceDiscovery.ServiceInstanceDiscovered += this.ServiceDiscovery_ServiceInstanceDiscovered;
this.serviceDiscovery.ServiceInstanceShutdown += this.ServiceDiscovery_ServiceInstanceShutdown;
this.serviceDiscovery.ServiceDiscovered += this.ServiceDiscovery_ServiceDiscovered;
return Task.Factory.StartNew(() => this.QueryServicesPeriodically(cancellationToken), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
Task IHostedService.StopAsync(CancellationToken cancellationToken)
{
this.serviceDiscovery.ServiceInstanceDiscovered -= this.ServiceDiscovery_ServiceInstanceDiscovered;
this.serviceDiscovery.ServiceInstanceShutdown -= this.ServiceDiscovery_ServiceInstanceShutdown;
this.serviceDiscovery.Dispose();
return Task.CompletedTask;
}
public IReadOnlyList<Uri>? Resolve(string service)
{
if (this.serviceLookup.TryGetValue(service, out var registration) &&
registration.Expiration > DateTime.UtcNow)
{
return [.. registration.Uris];
}
return default;
}
public IReadOnlyList<Uri>? QueryByServiceName(Func<string, bool> query)
{
var now = DateTime.UtcNow;
var returnList = new List<Uri>();
foreach (var serviceRegistration in this.serviceLookup.Values.Where(s => s.Expiration > now))
{
if (query(serviceRegistration.Name))
{
returnList.AddRange(serviceRegistration.Uris);
}
}
return returnList;
}
public void QueryAllServices()
{
this.serviceDiscovery.QueryAllServices();
}
private void ServiceDiscovery_ServiceDiscovered(object? _, DomainName e)
{
var labels = e.Labels;
e = labels.Count >= 1 && labels[^1] == "local"
? new DomainName([.. labels.Take(labels.Count - 1)])
: e;
this.serviceDiscovery.QueryServiceInstances(e);
}
private void ServiceDiscovery_ServiceInstanceShutdown(object? _, ServiceInstanceShutdownEventArgs e)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (e.ServiceInstanceName.Labels.Count <= 0 ||
e.ServiceInstanceName.Labels[0] is not string name)
{
return;
}
scopedLogger.LogDebug("Service {serviceName} has been shut down", name);
this.serviceLookup.TryRemove(name, out var __);
}
private void ServiceDiscovery_ServiceInstanceDiscovered(object? _, ServiceInstanceDiscoveryEventArgs e)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (e.ServiceInstanceName.Labels.Count <= 0 ||
e.ServiceInstanceName.Labels[0] is not string name)
{
return;
}
if (e.Message.Answers.OfType<PTRRecord>().FirstOrDefault() is not PTRRecord ptrRecord)
{
return;
}
scopedLogger.LogDebug("Discovered service {serviceName}", name);
var ttl = ptrRecord.TTL > MaxTTL
? MaxTTL
: ptrRecord.TTL;
var expiration = DateTime.UtcNow + ttl;
this.serviceLookup[name] = new ServiceRegistration { Name = name, Expiration = expiration, Uris = [.. BuildUrisFromResponse(e.Message)] };
}
private async ValueTask QueryServicesPeriodically(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
this.serviceDiscovery.QueryAllServices();
await Task.Delay(ScanFrequency, cancellationToken);
}
}
private static IEnumerable<Uri> BuildUrisFromResponse(Message srvResponse)
{
var srv = srvResponse.Answers.OfType<PTRRecord>().FirstOrDefault() is not null
? srvResponse.AdditionalRecords.OfType<SRVRecord>().FirstOrDefault()
: srvResponse.Answers.OfType<SRVRecord>().FirstOrDefault();
if (srv is not null)
{
var port = srv.Port;
var host = srv.Target;
var scheme = ParseSchemeFromServiceType(srv);
var ipv4 = srvResponse.AdditionalRecords
.OfType<ARecord>()
.Where(a => a.Name == host)
.Select(a => a.Address)
.Cast<IPAddress>();
var ipv6 = srvResponse.AdditionalRecords
.OfType<AAAARecord>()
.Where(a => a.Name == host)
.Select(a => a.Address)
.Cast<IPAddress>();
foreach (var ip in ipv4.Concat(ipv6))
{
var builder = new UriBuilder
{
Scheme = scheme,
Host = ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6
? $"[{ip}]"
: ip.ToString(),
Port = port
};
yield return builder.Uri;
}
}
}
private static string ParseSchemeFromServiceType(SRVRecord srvRecord)
{
// split "instance._http._tcp.local." → ["instance", "_http", "_tcp", "local"]
var labels = srvRecord.Target.Labels;
if (labels.Count <= 2) return "http";
return labels[^3] switch
{
"_https" => "https",
"_ws" => "ws",
"_wss" => "wss",
"_ftp" => "ftp",
_ => "http"
};
}
}
@@ -1,8 +0,0 @@
namespace Daybreak.Services.MDns;
internal sealed class ServiceRegistration
{
public required string Name { get; init; }
public required DateTime Expiration { get; init; }
public required IReadOnlyList<Uri> Uris { get; init; }
}
@@ -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);
@@ -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
-1
View File
@@ -16,7 +16,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MeaMod.DNS" />
<PackageReference Include="MemoryPack" />
<PackageReference Include="MemoryPack.Generator">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -1,19 +0,0 @@
using MeaMod.DNS.Multicast;
namespace Daybreak.Shared.Models;
public readonly struct DnsRegistrationToken : IDisposable
{
private readonly ServiceDiscovery serviceDiscovery = new();
internal DnsRegistrationToken(
ServiceProfile serviceProfile)
{
this.serviceDiscovery.Advertise(serviceProfile);
this.serviceDiscovery.Announce(serviceProfile);
}
public void Dispose()
{
this.serviceDiscovery.Dispose();
}
}
@@ -0,0 +1,27 @@
namespace Daybreak.Shared.Services.Api;
/// <summary>
/// Service for discovering running Daybreak API instances by scanning known ports.
/// Replaces mDNS-based service discovery for cross-platform compatibility.
/// </summary>
public interface IApiScanningService
{
/// <summary>
/// Gets the URI for a Daybreak API instance associated with the given process ID.
/// </summary>
/// <param name="processId">The process ID of the Guild Wars instance.</param>
/// <returns>The API URI if found, null otherwise.</returns>
Uri? GetApiUriByProcessId(int processId);
/// <summary>
/// Gets all discovered Daybreak API URIs matching a predicate.
/// </summary>
/// <param name="predicate">A predicate to filter discovered services by process ID.</param>
/// <returns>A list of matching URIs, or null if none found.</returns>
IReadOnlyList<(int ProcessId, Uri Uri)>? QueryByProcessId(Func<int, bool> predicate);
/// <summary>
/// Triggers an immediate scan for Daybreak API instances.
/// </summary>
void RequestScan();
}
@@ -0,0 +1,17 @@
namespace Daybreak.Shared.Services.Api;
/// <summary>
/// Platform-specific provider for converting process IDs.
/// On Windows, this is a pass-through. On Linux, this maps Wine PIDs to Linux system PIDs.
/// </summary>
public interface IPidProvider
{
/// <summary>
/// Converts a reported process ID (e.g., from the Daybreak API health endpoint)
/// to the actual system process ID.
/// </summary>
/// <param name="reportedPid">The process ID reported by the API (may be a Wine PID on Linux).</param>
/// <param name="executableName">The executable name to search for (e.g., "Gw.exe").</param>
/// <returns>The system process ID, or the original PID if conversion fails or is not needed.</returns>
int ResolveSystemPid(int reportedPid, string executableName);
}
@@ -1,7 +0,0 @@
using Daybreak.Shared.Models;
namespace Daybreak.Shared.Services.MDns;
public interface IMDomainNameService
{
DnsRegistrationToken RegisterDomain(string service, ushort port, string subType, string protocol = "_http._tcp");
}
@@ -1,9 +0,0 @@
namespace Daybreak.Shared.Services.MDns;
public interface IMDomainRegistrar
{
IReadOnlyList<Uri>? Resolve(string service);
IReadOnlyList<Uri>? QueryByServiceName(Func<string, bool> query);
void QueryAllServices();
}
@@ -1,18 +0,0 @@
using Daybreak.Shared.Models;
using MeaMod.DNS.Multicast;
namespace Daybreak.Shared.Services.MDns;
public sealed class MDomainNameService
: IMDomainNameService
{
public DnsRegistrationToken RegisterDomain(string service, ushort port, string subType, string protocol = "_http._tcp")
{
var profile = new ServiceProfile(service, protocol, port);
profile.AddProperty("path", "/");
profile.AddProperty("name", service);
profile.Subtypes.Add(subType);
return new DnsRegistrationToken(profile);
}
}
@@ -1,12 +1,12 @@
using Daybreak.Configuration;
using Daybreak.Extensions;
using Daybreak.Services.MDns;
using Daybreak.Windows.Services.Api;
using Daybreak.Shared.Models.Plugins;
using Daybreak.Shared.Services.Credentials;
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.Shortcuts;
@@ -77,7 +77,7 @@ public sealed class WindowsPlatformConfiguration : PluginConfigurationBase
services.AddScoped<IRegistryService, RegistryService>();
services.AddSingleton<ISystemThemeDetector, SystemThemeDetector>();
services.AddSingleton<ISevenZipExtractor, SevenZipArchiveExtractor>();
services.AddHostedSingleton<IMDomainRegistrar, MDomainRegistrar>();
services.AddSingleton<IPidProvider, PidProvider>();
services.AddSingleton<IWindowManipulationService, WindowManipulationService>();
services.AddSingleton<ICrashDumpService, CrashDumpService>();
services.AddSingleton<IDaybreakRestartingService, DaybreakRestartingService>();
@@ -0,0 +1,17 @@
using Daybreak.Shared.Services.Api;
namespace Daybreak.Windows.Services.Api;
/// <summary>
/// Windows implementation of <see cref="IPidProvider"/>.
/// On Windows, the reported PID is the actual system PID, so this is a pass-through.
/// </summary>
public sealed class PidProvider : IPidProvider
{
/// <inheritdoc />
public int ResolveSystemPid(int reportedPid, string executableName)
{
// On Windows, the reported PID is the actual system PID
return reportedPid;
}
}
-2
View File
@@ -10,8 +10,6 @@
<PackageVersion Include="FluentAssertions" Version="8.8.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
<PackageVersion Include="ini-parser-netstandard" Version="2.5.3" />
<PackageVersion Include="ksemenenko.ColorThief" Version="1.1.1.4" />
<PackageVersion Include="MeaMod.DNS" Version="1.0.71" />
<PackageVersion Include="MegaApiClient" Version="1.10.5" />
<PackageVersion Include="MemoryPack" Version="1.21.4" />
<PackageVersion Include="MemoryPack.Generator" Version="1.21.4" />