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