mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-07-15 15:19:57 +00:00
@@ -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>();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
+35
-49
@@ -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 (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 <see cref="IWinePidMapper"/>.
|
||||
/// 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.
|
||||
/// </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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user