From 8072790cfd194f9e346216c4d7ad9bb781cd808a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 18 Feb 2026 20:31:12 +0000 Subject: [PATCH 1/4] Bump version to 0.9.10.2 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 80085eae..ea946afa 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ $(NoWarn);IL2104;IL3053;IL3000;IL3002;NU1701;CS0108 - 0.9.10.1 + 0.9.10.2 enable enable From aaad10a7550faf9aadf831344556e95a981368f7 Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Thu, 19 Feb 2026 06:52:53 -0800 Subject: [PATCH 2/4] Fix companion view attach after restart (Closes #1452) (#1453) --- Daybreak.Core/Views/LaunchView.razor.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Daybreak.Core/Views/LaunchView.razor.cs b/Daybreak.Core/Views/LaunchView.razor.cs index cae75d5c..6d80cd75 100644 --- a/Daybreak.Core/Views/LaunchView.razor.cs +++ b/Daybreak.Core/Views/LaunchView.razor.cs @@ -541,12 +541,6 @@ private async Task AttachContext( CancellationToken cancellationToken ) { - // Only attach if Daybreak API was enabled for this launch - if (!context.EnabledMods.OfType().Any()) - { - return; - } - using var notificationToken = this.notificationService.NotifyInformation( title: "Attaching to Guild Wars process...", description: "Attempting to attach to Guild Wars process" From 7503bfa95a58849db3fe34c15201fab9496e1de3 Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Thu, 19 Feb 2026 08:08:09 -0800 Subject: [PATCH 3/4] Deprecate mDNS scanning (Closes #1454) (#1455) --- .../Services/ApiAdvertisingService.cs | 56 ------ .../WebApplicationBuilderExtensions.cs | 3 - .../Configuration/ProjectConfiguration.cs | 3 +- Daybreak.Core/Daybreak.Core.csproj | 1 - .../Services/Api/ApiScanningService.cs | 84 ++++---- .../Services/Api/DaybreakApiService.cs | 20 +- .../Services/MDns/MDomainRegistrar.cs | 180 ------------------ .../Services/MDns/ServiceRegistration.cs | 8 - .../LinuxPlatformConfiguration.cs | 6 +- Daybreak.Linux/Services/Api/PidProvider.cs | 22 +++ .../GuildWarsProcessFinder.cs | 13 -- Daybreak.Linux/Services/Wine/WinePidMapper.cs | 15 -- Daybreak.Shared/Daybreak.Shared.csproj | 1 - .../Models/DnsRegistrationToken.cs | 19 -- .../Services/Api/IApiScanningService.cs | 27 +++ Daybreak.Shared/Services/Api/IPidProvider.cs | 17 ++ .../Services/MDns/IMDomainNameService.cs | 7 - .../Services/MDns/IMDomainRegistrar.cs | 9 - .../Services/MDns/MDomainNameService.cs | 18 -- .../WindowsPlatformConfiguration.cs | 6 +- Daybreak.Windows/Services/Api/PidProvider.cs | 17 ++ Directory.Packages.props | 2 - 22 files changed, 132 insertions(+), 402 deletions(-) delete mode 100644 Daybreak.API/Services/ApiAdvertisingService.cs rename Daybreak.Linux/Services/MDns/PortScanningDomainRegistrar.cs => Daybreak.Core/Services/Api/ApiScanningService.cs (60%) delete mode 100644 Daybreak.Core/Services/MDns/MDomainRegistrar.cs delete mode 100644 Daybreak.Core/Services/MDns/ServiceRegistration.cs create mode 100644 Daybreak.Linux/Services/Api/PidProvider.cs delete mode 100644 Daybreak.Shared/Models/DnsRegistrationToken.cs create mode 100644 Daybreak.Shared/Services/Api/IApiScanningService.cs create mode 100644 Daybreak.Shared/Services/Api/IPidProvider.cs delete mode 100644 Daybreak.Shared/Services/MDns/IMDomainNameService.cs delete mode 100644 Daybreak.Shared/Services/MDns/IMDomainRegistrar.cs delete mode 100644 Daybreak.Shared/Services/MDns/MDomainNameService.cs create mode 100644 Daybreak.Windows/Services/Api/PidProvider.cs diff --git a/Daybreak.API/Services/ApiAdvertisingService.cs b/Daybreak.API/Services/ApiAdvertisingService.cs deleted file mode 100644 index 33cbc65a..00000000 --- a/Daybreak.API/Services/ApiAdvertisingService.cs +++ /dev/null @@ -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 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() ?? throw new InvalidOperationException("Server does not support addresses feature."); - private readonly IMDomainNameService mDnsService = mDnsService; - private readonly IHostApplicationLifetime lifetime = lifetime; - private readonly ILogger 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); - } -} diff --git a/Daybreak.API/Services/WebApplicationBuilderExtensions.cs b/Daybreak.API/Services/WebApplicationBuilderExtensions.cs index c3a3065f..b32efb19 100644 --- a/Daybreak.API/Services/WebApplicationBuilderExtensions.cs +++ b/Daybreak.API/Services/WebApplicationBuilderExtensions.cs @@ -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 WebApplicationBuilder WithDaybreakServices(this WebApplicationBuil { builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddHostedService(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/Daybreak.Core/Configuration/ProjectConfiguration.cs b/Daybreak.Core/Configuration/ProjectConfiguration.cs index 33f76166..34a4d81e 100644 --- a/Daybreak.Core/Configuration/ProjectConfiguration.cs +++ b/Daybreak.Core/Configuration/ProjectConfiguration.cs @@ -67,7 +67,6 @@ 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 override void RegisterServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -255,6 +253,7 @@ public override void RegisterServices(IServiceCollection services) services.AddHostedSingleton(); services.AddHostedSingleton(); services.AddHostedSingleton(); + services.AddHostedSingleton(); services.AddHostedSingleton(); services.AddHostedService(); services.AddHostedService(); diff --git a/Daybreak.Core/Daybreak.Core.csproj b/Daybreak.Core/Daybreak.Core.csproj index d93033b8..1edbd421 100644 --- a/Daybreak.Core/Daybreak.Core.csproj +++ b/Daybreak.Core/Daybreak.Core.csproj @@ -40,7 +40,6 @@ - diff --git a/Daybreak.Linux/Services/MDns/PortScanningDomainRegistrar.cs b/Daybreak.Core/Services/Api/ApiScanningService.cs similarity index 60% rename from Daybreak.Linux/Services/MDns/PortScanningDomainRegistrar.cs rename to Daybreak.Core/Services/Api/ApiScanningService.cs index 90d2eff7..1d1eb918 100644 --- a/Daybreak.Linux/Services/MDns/PortScanningDomainRegistrar.cs +++ b/Daybreak.Core/Services/Api/ApiScanningService.cs @@ -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; /// -/// Linux-specific implementation of . -/// Since mDNS announcements from Wine don't propagate to the host, -/// this implementation scans a known port range (5080–5100) 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 . +/// Scans a known port range (5080–5100) 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. /// -public sealed class PortScanningDomainRegistrar( - IWinePidMapper winePidMapper, - ILogger logger) - : IMDomainRegistrar, IHostedService, IDisposable +public sealed class ApiScanningService( + IPidProvider pidProvider, + ILogger 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 logger = logger; + private readonly IPidProvider pidProvider = pidProvider; + private readonly ILogger 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> discoveredServices = []; + private volatile Dictionary 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 void Dispose() this.httpClient.Dispose(); } - public IReadOnlyList? 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? QueryByServiceName(Func query) + public IReadOnlyList<(int ProcessId, Uri Uri)>? QueryByProcessId(Func predicate) { - var results = new List(); - 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 @@ private async Task ScanPeriodically(CancellationToken cancellationToken) private async Task ScanPortsAsync(CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(); - var newServices = new Dictionary>(); + var newApis = new Dictionary(); using var timeoutCts = new CancellationTokenSource(ProbeTimeout); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); @@ -129,36 +124,27 @@ private async Task ScanPortsAsync(CancellationToken cancellationToken) 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); - - if (!newServices.TryGetValue(serviceName, out var uriList)) - { - uriList = []; - newServices[serviceName] = uriList; - } + reportedPid.Value, + systemPid); - 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) diff --git a/Daybreak.Core/Services/Api/DaybreakApiService.cs b/Daybreak.Core/Services/Api/DaybreakApiService.cs index 7bb90ed2..60c1312b 100644 --- a/Daybreak.Core/Services/Api/DaybreakApiService.cs +++ b/Daybreak.Core/Services/Api/DaybreakApiService.cs @@ -5,7 +5,6 @@ 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 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 scopedApiClient = scopedApiClient.ThrowIfNull(); @@ -124,12 +120,10 @@ public Task OnCustomManagement(CancellationToken cancellationToken) 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 Task OnCustomManagement(CancellationToken cancellationToken) public async Task 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 Task OnCustomManagement(CancellationToken cancellationToken) public void RequestInstancesAnnouncement() { - this.mDomainRegistrar.QueryAllServices(); + this.apiScanningService.RequestScan(); } public Task OnGuildWarsCreated(GuildWarsCreatedContext guildWarsCreatedContext, CancellationToken cancellationToken) => diff --git a/Daybreak.Core/Services/MDns/MDomainRegistrar.cs b/Daybreak.Core/Services/MDns/MDomainRegistrar.cs deleted file mode 100644 index c81ea651..00000000 --- a/Daybreak.Core/Services/MDns/MDomainRegistrar.cs +++ /dev/null @@ -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 logger) - : IMDomainRegistrar, IHostedService -{ - private static readonly TimeSpan ScanFrequency = TimeSpan.FromSeconds(15); - private static readonly TimeSpan MaxTTL = TimeSpan.FromSeconds(20); - - private readonly ConcurrentDictionary serviceLookup = []; - private readonly ServiceDiscovery serviceDiscovery = new(); - private readonly ILogger 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? Resolve(string service) - { - if (this.serviceLookup.TryGetValue(service, out var registration) && - registration.Expiration > DateTime.UtcNow) - { - return [.. registration.Uris]; - } - - return default; - } - - public IReadOnlyList? QueryByServiceName(Func query) - { - var now = DateTime.UtcNow; - var returnList = new List(); - 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().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 BuildUrisFromResponse(Message srvResponse) - { - var srv = srvResponse.Answers.OfType().FirstOrDefault() is not null - ? srvResponse.AdditionalRecords.OfType().FirstOrDefault() - : srvResponse.Answers.OfType().FirstOrDefault(); - if (srv is not null) - { - var port = srv.Port; - var host = srv.Target; - var scheme = ParseSchemeFromServiceType(srv); - var ipv4 = srvResponse.AdditionalRecords - .OfType() - .Where(a => a.Name == host) - .Select(a => a.Address) - .Cast(); - - var ipv6 = srvResponse.AdditionalRecords - .OfType() - .Where(a => a.Name == host) - .Select(a => a.Address) - .Cast(); - - 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" - }; - } -} diff --git a/Daybreak.Core/Services/MDns/ServiceRegistration.cs b/Daybreak.Core/Services/MDns/ServiceRegistration.cs deleted file mode 100644 index 0352be6a..00000000 --- a/Daybreak.Core/Services/MDns/ServiceRegistration.cs +++ /dev/null @@ -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 Uris { get; init; } -} diff --git a/Daybreak.Linux/Configuration/LinuxPlatformConfiguration.cs b/Daybreak.Linux/Configuration/LinuxPlatformConfiguration.cs index b96aa836..4cf42969 100644 --- a/Daybreak.Linux/Configuration/LinuxPlatformConfiguration.cs +++ b/Daybreak.Linux/Configuration/LinuxPlatformConfiguration.cs @@ -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.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 override void RegisterServices(IServiceCollection services) services.AddScoped(); services.AddSingleton(); services.AddSingleton(); - services.AddHostedSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Daybreak.Linux/Services/Api/PidProvider.cs b/Daybreak.Linux/Services/Api/PidProvider.cs new file mode 100644 index 00000000..20a5e66c --- /dev/null +++ b/Daybreak.Linux/Services/Api/PidProvider.cs @@ -0,0 +1,22 @@ +using Daybreak.Linux.Services.Wine; +using Daybreak.Shared.Services.Api; + +namespace Daybreak.Linux.Services.Api; + +/// +/// Linux implementation of . +/// On Linux, the reported PID is a Wine-internal PID that must be +/// converted to the actual Linux system PID via . +/// +public sealed class PidProvider(IWinePidMapper winePidMapper) : IPidProvider +{ + private readonly IWinePidMapper winePidMapper = winePidMapper; + + /// + 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; + } +} diff --git a/Daybreak.Linux/Services/ApplicationLauncher/GuildWarsProcessFinder.cs b/Daybreak.Linux/Services/ApplicationLauncher/GuildWarsProcessFinder.cs index 507b71f6..6275f320 100644 --- a/Daybreak.Linux/Services/ApplicationLauncher/GuildWarsProcessFinder.cs +++ b/Daybreak.Linux/Services/ApplicationLauncher/GuildWarsProcessFinder.cs @@ -121,19 +121,6 @@ params LaunchConfigurationWithCredentials[] configurations 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); diff --git a/Daybreak.Linux/Services/Wine/WinePidMapper.cs b/Daybreak.Linux/Services/Wine/WinePidMapper.cs index 0bd57658..8638a3ac 100644 --- a/Daybreak.Linux/Services/Wine/WinePidMapper.cs +++ b/Daybreak.Linux/Services/Wine/WinePidMapper.cs @@ -19,8 +19,6 @@ ILogger logger /// public int? WinePidToLinuxPid(int winePid, string executableName) { - var prefixPath = this.winePrefixManager.GetWinePrefixPath(); - try { foreach (var procDir in Directory.EnumerateDirectories("/proc")) @@ -45,19 +43,6 @@ ILogger logger 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 diff --git a/Daybreak.Shared/Daybreak.Shared.csproj b/Daybreak.Shared/Daybreak.Shared.csproj index ea03c5d3..c3e33431 100644 --- a/Daybreak.Shared/Daybreak.Shared.csproj +++ b/Daybreak.Shared/Daybreak.Shared.csproj @@ -16,7 +16,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Daybreak.Shared/Models/DnsRegistrationToken.cs b/Daybreak.Shared/Models/DnsRegistrationToken.cs deleted file mode 100644 index 19a58973..00000000 --- a/Daybreak.Shared/Models/DnsRegistrationToken.cs +++ /dev/null @@ -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(); - } -} diff --git a/Daybreak.Shared/Services/Api/IApiScanningService.cs b/Daybreak.Shared/Services/Api/IApiScanningService.cs new file mode 100644 index 00000000..cdb50d55 --- /dev/null +++ b/Daybreak.Shared/Services/Api/IApiScanningService.cs @@ -0,0 +1,27 @@ +namespace Daybreak.Shared.Services.Api; + +/// +/// Service for discovering running Daybreak API instances by scanning known ports. +/// Replaces mDNS-based service discovery for cross-platform compatibility. +/// +public interface IApiScanningService +{ + /// + /// Gets the URI for a Daybreak API instance associated with the given process ID. + /// + /// The process ID of the Guild Wars instance. + /// The API URI if found, null otherwise. + Uri? GetApiUriByProcessId(int processId); + + /// + /// Gets all discovered Daybreak API URIs matching a predicate. + /// + /// A predicate to filter discovered services by process ID. + /// A list of matching URIs, or null if none found. + IReadOnlyList<(int ProcessId, Uri Uri)>? QueryByProcessId(Func predicate); + + /// + /// Triggers an immediate scan for Daybreak API instances. + /// + void RequestScan(); +} diff --git a/Daybreak.Shared/Services/Api/IPidProvider.cs b/Daybreak.Shared/Services/Api/IPidProvider.cs new file mode 100644 index 00000000..0423e6d7 --- /dev/null +++ b/Daybreak.Shared/Services/Api/IPidProvider.cs @@ -0,0 +1,17 @@ +namespace Daybreak.Shared.Services.Api; + +/// +/// Platform-specific provider for converting process IDs. +/// On Windows, this is a pass-through. On Linux, this maps Wine PIDs to Linux system PIDs. +/// +public interface IPidProvider +{ + /// + /// Converts a reported process ID (e.g., from the Daybreak API health endpoint) + /// to the actual system process ID. + /// + /// The process ID reported by the API (may be a Wine PID on Linux). + /// The executable name to search for (e.g., "Gw.exe"). + /// The system process ID, or the original PID if conversion fails or is not needed. + int ResolveSystemPid(int reportedPid, string executableName); +} diff --git a/Daybreak.Shared/Services/MDns/IMDomainNameService.cs b/Daybreak.Shared/Services/MDns/IMDomainNameService.cs deleted file mode 100644 index 17bd3fce..00000000 --- a/Daybreak.Shared/Services/MDns/IMDomainNameService.cs +++ /dev/null @@ -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"); -} diff --git a/Daybreak.Shared/Services/MDns/IMDomainRegistrar.cs b/Daybreak.Shared/Services/MDns/IMDomainRegistrar.cs deleted file mode 100644 index d2d24373..00000000 --- a/Daybreak.Shared/Services/MDns/IMDomainRegistrar.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Daybreak.Shared.Services.MDns; -public interface IMDomainRegistrar -{ - IReadOnlyList? Resolve(string service); - - IReadOnlyList? QueryByServiceName(Func query); - - void QueryAllServices(); -} diff --git a/Daybreak.Shared/Services/MDns/MDomainNameService.cs b/Daybreak.Shared/Services/MDns/MDomainNameService.cs deleted file mode 100644 index b03a7b92..00000000 --- a/Daybreak.Shared/Services/MDns/MDomainNameService.cs +++ /dev/null @@ -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); - } -} diff --git a/Daybreak.Windows/Configuration/WindowsPlatformConfiguration.cs b/Daybreak.Windows/Configuration/WindowsPlatformConfiguration.cs index 5b03800f..ab43b6fd 100644 --- a/Daybreak.Windows/Configuration/WindowsPlatformConfiguration.cs +++ b/Daybreak.Windows/Configuration/WindowsPlatformConfiguration.cs @@ -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 override void RegisterServices(IServiceCollection services) services.AddScoped(); services.AddSingleton(); services.AddSingleton(); - services.AddHostedSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Daybreak.Windows/Services/Api/PidProvider.cs b/Daybreak.Windows/Services/Api/PidProvider.cs new file mode 100644 index 00000000..1b76987f --- /dev/null +++ b/Daybreak.Windows/Services/Api/PidProvider.cs @@ -0,0 +1,17 @@ +using Daybreak.Shared.Services.Api; + +namespace Daybreak.Windows.Services.Api; + +/// +/// Windows implementation of . +/// On Windows, the reported PID is the actual system PID, so this is a pass-through. +/// +public sealed class PidProvider : IPidProvider +{ + /// + public int ResolveSystemPid(int reportedPid, string executableName) + { + // On Windows, the reported PID is the actual system PID + return reportedPid; + } +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 36562dd0..ac2f7c83 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,8 +10,6 @@ - - From fc5fd926b4883cd7f57f46dcec5f9b40398700f2 Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Thu, 19 Feb 2026 08:14:39 -0800 Subject: [PATCH 4/4] Simplify readychecker (#1457) * Fix windows ready checker (Closes #1456) * Add extra debugging line * Extra debug info --- .../ApplicationLauncher.cs | 1 + .../GuildWarsReadyChecker.cs | 83 +------------------ 2 files changed, 2 insertions(+), 82 deletions(-) diff --git a/Daybreak.Core/Services/ApplicationLauncher/ApplicationLauncher.cs b/Daybreak.Core/Services/ApplicationLauncher/ApplicationLauncher.cs index e9d30055..52d73cc1 100644 --- a/Daybreak.Core/Services/ApplicationLauncher/ApplicationLauncher.cs +++ b/Daybreak.Core/Services/ApplicationLauncher/ApplicationLauncher.cs @@ -439,6 +439,7 @@ is null return (default, []); } + scopedLogger.LogDebug("Waiting for Guild Wars to be ready..."); var sw = Stopwatch.StartNew(); var isReady = await this.guildWarsReadyChecker.WaitForReady( process, diff --git a/Daybreak.Windows/Services/ApplicationLauncher/GuildWarsReadyChecker.cs b/Daybreak.Windows/Services/ApplicationLauncher/GuildWarsReadyChecker.cs index 773f0077..e995e438 100644 --- a/Daybreak.Windows/Services/ApplicationLauncher/GuildWarsReadyChecker.cs +++ b/Daybreak.Windows/Services/ApplicationLauncher/GuildWarsReadyChecker.cs @@ -3,9 +3,6 @@ using System.Core.Extensions; using System.Diagnostics; using System.Extensions.Core; -using System.Runtime.InteropServices; -using System.Text; -using static Daybreak.Windows.Utils.NativeMethods; namespace Daybreak.Windows.Services.ApplicationLauncher; @@ -17,8 +14,6 @@ internal sealed class GuildWarsReadyChecker( ILogger logger ) : IGuildWarsReadyChecker { - private const double LaunchMemoryThreshold = 200000000; - private readonly ILogger logger = logger.ThrowIfNull(); public async Task WaitForReady(Process process, TimeSpan timeout, CancellationToken cancellationToken) @@ -41,87 +36,11 @@ public async Task WaitForReady(Process process, TimeSpan timeout, Cancella continue; } - var windows = GetRootWindowsOfProcess(process.Id) - .Select(root => (root, GetChildWindows(root))) - .SelectMany(tuple => - { - tuple.Item2.Add(tuple.root); - return tuple.Item2; - }) - .Select(GetWindowTitle).ToList(); - - /* - * Detect when the game window has shown. Because both the updater and the game window are called Guild Wars, - * we need to look at the other windows created by the process. Especially, we need to detect the input windows - * to check when the game is ready to accept input - */ - if (!windows.Contains("Guild Wars Reforged")) - { - continue; - } - - var virtualMemory = process.VirtualMemorySize64; - if (virtualMemory < LaunchMemoryThreshold) - { - continue; - } - + scopedLogger.LogInformation("Guild Wars process is ready and running"); return true; } scopedLogger.LogError("Timed out waiting for Guild Wars to be ready"); return false; } - - private static List GetRootWindowsOfProcess(int pid) - { - var rootWindows = GetChildWindows(IntPtr.Zero); - var dsProcRootWindows = new List(); - foreach (IntPtr hWnd in rootWindows) - { - _ = GetWindowThreadProcessId(hWnd, out var lpdwProcessId); - if (lpdwProcessId == pid) - dsProcRootWindows.Add(hWnd); - } - - return dsProcRootWindows; - } - - private static List GetChildWindows(IntPtr parent) - { - var result = new List(); - var listHandle = GCHandle.Alloc(result); - try - { - var childProc = new Win32Callback(EnumWindow); - EnumChildWindows(parent, childProc, GCHandle.ToIntPtr(listHandle)); - } - finally - { - if (listHandle.IsAllocated) - listHandle.Free(); - } - - return result; - } - - private static string GetWindowTitle(IntPtr hwnd) - { - var titleLength = GetWindowTextLength(hwnd); - var titleBuffer = new StringBuilder(titleLength); - _ = GetWindowText(hwnd, titleBuffer, titleLength + 1); - return titleBuffer.ToString(); - } - - private static bool EnumWindow(IntPtr handle, IntPtr pointer) - { - var gch = GCHandle.FromIntPtr(pointer); - if (gch.Target is not List list) - { - return false; - } - - list.Add(handle); - return true; - } }