mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-07-15 15:19:57 +00:00
Fix multibox support on Linux (#1566)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Extensions.Core;
|
||||
using System.Logging;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using Daybreak.API.Configuration;
|
||||
using Daybreak.API.Extensions;
|
||||
@@ -22,8 +23,12 @@ namespace Daybreak.API;
|
||||
public class EntryPoint
|
||||
{
|
||||
private const int StartPort = 5080;
|
||||
private const string GwcaDllName = "gwca.dll";
|
||||
private static readonly TimeSpan InitializationTimeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly CancellationTokenSource CancellationTokenSource = new();
|
||||
private static readonly object GwcaResolverLock = new();
|
||||
private static IntPtr gwcaHandle;
|
||||
private static bool gwcaResolverRegistered;
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "ThreadInit"), STAThread]
|
||||
[RequiresUnreferencedCode("The handler uses a static method that gets referenced, so there's no unreferenced code to worry about")]
|
||||
@@ -192,7 +197,8 @@ public class EntryPoint
|
||||
{
|
||||
if (module.ModuleName?.Equals("gwca.dll", StringComparison.OrdinalIgnoreCase) is true)
|
||||
{
|
||||
NativeLibrary.Load(module.FileName);
|
||||
var handle = NativeLibrary.Load(module.FileName);
|
||||
RegisterGwcaResolver(handle, module.FileName, logger);
|
||||
logger.LogDebug($"Reused already-loaded gwca.dll from {module.FileName}");
|
||||
return;
|
||||
}
|
||||
@@ -213,7 +219,8 @@ public class EntryPoint
|
||||
var gwcaPath = Path.Combine(daybreakApiDir, "gwca.dll");
|
||||
if (File.Exists(gwcaPath))
|
||||
{
|
||||
NativeLibrary.Load(gwcaPath);
|
||||
var handle = NativeLibrary.Load(gwcaPath);
|
||||
RegisterGwcaResolver(handle, gwcaPath, logger);
|
||||
logger.LogDebug($"Preloaded gwca.dll from {gwcaPath}");
|
||||
}
|
||||
else
|
||||
@@ -221,4 +228,31 @@ public class EntryPoint
|
||||
logger.LogError($"gwca.dll not found at {gwcaPath}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterGwcaResolver(IntPtr handle, string path, ScopedLogger<EntryPoint> logger)
|
||||
{
|
||||
lock (GwcaResolverLock)
|
||||
{
|
||||
gwcaHandle = handle;
|
||||
if (gwcaResolverRegistered)
|
||||
{
|
||||
logger.LogDebug($"Updated gwca.dll resolver handle for {path}");
|
||||
return;
|
||||
}
|
||||
|
||||
NativeLibrary.SetDllImportResolver(typeof(GWCA).Assembly, ResolveNativeLibrary);
|
||||
gwcaResolverRegistered = true;
|
||||
logger.LogDebug($"Registered gwca.dll import resolver for {path}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr ResolveNativeLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
|
||||
{
|
||||
if (string.Equals(libraryName, GwcaDllName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return gwcaHandle;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ public sealed class ApiScanningService(
|
||||
}
|
||||
|
||||
// Convert the reported PID (Wine PID on Linux) to system PID
|
||||
var systemPid = this.pidProvider.ResolveSystemPid(reportedPid.Value, GuildWarsExecutable);
|
||||
var systemPid = this.pidProvider.ResolveSystemPid(reportedPid.Value, GuildWarsExecutable, port);
|
||||
var uri = new Uri($"http://localhost:{port}");
|
||||
|
||||
scopedLogger.LogDebug(
|
||||
|
||||
@@ -133,9 +133,9 @@ internal sealed class ToolboxService(
|
||||
|
||||
public Task OnGuildWarsStartingDisabled(GuildWarsStartingDisabledContext guildWarsStartingDisabledContext, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public Task OnGuildWarsCreated(GuildWarsCreatedContext guildWarsCreatedContext, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task OnGuildWarsCreated(GuildWarsCreatedContext guildWarsCreatedContext, CancellationToken cancellationToken) => this.LaunchToolbox(guildWarsCreatedContext.ApplicationLauncherContext.Process, cancellationToken);
|
||||
|
||||
public Task OnGuildWarsStarted(GuildWarsStartedContext guildWarsStartedContext, CancellationToken cancellationToken) => this.LaunchToolbox(guildWarsStartedContext.ApplicationLauncherContext.Process, cancellationToken);
|
||||
public Task OnGuildWarsStarted(GuildWarsStartedContext guildWarsStartedContext, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public IEnumerable<string> GetCustomArguments() => [];
|
||||
|
||||
|
||||
@@ -1,22 +1,191 @@
|
||||
using Daybreak.Linux.Services.Wine;
|
||||
using Daybreak.Shared.Services.Api;
|
||||
using System.Globalization;
|
||||
|
||||
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"/>.
|
||||
/// On Linux, the reported PID is a Wine-internal PID. The most reliable way to map an
|
||||
/// API instance to its Linux process is via the TCP port it listens on: we resolve the
|
||||
/// owning Linux PID from the listening socket. This disambiguates multiple concurrent
|
||||
/// Guild Wars instances, which a Wine-PID-by-name lookup cannot do.
|
||||
/// </summary>
|
||||
public sealed class PidProvider(IWinePidMapper winePidMapper) : IPidProvider
|
||||
{
|
||||
private readonly IWinePidMapper winePidMapper = winePidMapper;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int ResolveSystemPid(int reportedPid, string executableName)
|
||||
public int ResolveSystemPid(int reportedPid, string executableName, int? port = null)
|
||||
{
|
||||
// On Linux, the reported PID is a Wine PID. Convert to Linux system PID.
|
||||
if (port is not null && TryGetProcessIdForTcpListener(port.Value, executableName) is { } listenerPid)
|
||||
{
|
||||
return listenerPid;
|
||||
}
|
||||
|
||||
// Fallback: the reported PID is a Wine PID. Convert to Linux system PID by name.
|
||||
var linuxPid = this.winePidMapper.WinePidToLinuxPid(reportedPid, executableName);
|
||||
return linuxPid ?? reportedPid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the Linux PID that owns the listening socket on <paramref name="port"/>.
|
||||
/// Under Wine the listening socket fd is shared between the game process and the
|
||||
/// shared <c>wineserver</c> process, so candidate processes are filtered by
|
||||
/// <paramref name="executableName"/> to select the game process and exclude wineserver.
|
||||
/// </summary>
|
||||
private static int? TryGetProcessIdForTcpListener(int port, string executableName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var socketInodes = GetTcpListenerSocketInodes("/proc/net/tcp", port)
|
||||
.Concat(GetTcpListenerSocketInodes("/proc/net/tcp6", port))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
if (socketInodes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var procDir in Directory.EnumerateDirectories("/proc"))
|
||||
{
|
||||
if (!int.TryParse(Path.GetFileName(procDir), out var pid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only consider processes whose command line references the target
|
||||
// executable. This excludes wineserver (which shares the socket fd)
|
||||
// and limits the fd scan to the handful of Guild Wars processes.
|
||||
if (!ProcessReferencesExecutable(procDir, executableName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ProcessOwnsSocket(procDir, socketInodes))
|
||||
{
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ProcessReferencesExecutable(string procDir, string executableName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cmdline = File.ReadAllText(Path.Combine(procDir, "cmdline"));
|
||||
return cmdline.Contains(executableName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ProcessOwnsSocket(string procDir, HashSet<string> socketInodes)
|
||||
{
|
||||
var fdDir = Path.Combine(procDir, "fd");
|
||||
if (!Directory.Exists(fdDir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var fd in Directory.EnumerateFiles(fdDir))
|
||||
{
|
||||
string? linkTarget;
|
||||
try
|
||||
{
|
||||
linkTarget = new FileInfo(fd).LinkTarget;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (linkTarget is not null &&
|
||||
TryParseSocketInode(linkTarget, out var inode) &&
|
||||
socketInodes.Contains(inode))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetTcpListenerSocketInodes(string procNetPath, int port)
|
||||
{
|
||||
if (!File.Exists(procNetPath))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var line in File.ReadLines(procNetPath).Skip(1))
|
||||
{
|
||||
var columns = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (columns.Length < 10 ||
|
||||
!IsListeningSocket(columns[3]) ||
|
||||
!TryParsePort(columns[1], out var socketPort) ||
|
||||
socketPort != port)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return columns[9];
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParsePort(string localAddress, out int port)
|
||||
{
|
||||
port = default;
|
||||
var separatorIndex = localAddress.LastIndexOf(':');
|
||||
if (separatorIndex < 0 ||
|
||||
separatorIndex == localAddress.Length - 1 ||
|
||||
!int.TryParse(localAddress[(separatorIndex + 1)..], NumberStyles.HexNumber, null, out port))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsListeningSocket(string state) => state.Equals("0A", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool TryParseSocketInode(string linkTarget, out string inode)
|
||||
{
|
||||
inode = string.Empty;
|
||||
const string socketPrefix = "socket:[";
|
||||
if (!linkTarget.StartsWith(socketPrefix, StringComparison.Ordinal) || !linkTarget.EndsWith(']'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
inode = linkTarget[socketPrefix.Length..^1];
|
||||
return inode.Length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,8 +193,9 @@ public class DaybreakInjector(
|
||||
// Convert Wine PID to Linux PID so the rest of Daybreak can use Process.GetProcessById()
|
||||
if (processId > 0)
|
||||
{
|
||||
var executableName = Path.GetFileName(executablePath);
|
||||
var linuxPid = this.winePidMapper.WinePidToLinuxPid(processId, executableName);
|
||||
// Match on the full executable path so concurrent Guild Wars instances
|
||||
// (different install directories) are not confused for one another.
|
||||
var linuxPid = this.winePidMapper.WinePidToLinuxPid(processId, executablePath);
|
||||
if (linuxPid is not null)
|
||||
{
|
||||
scopedLogger.LogInformation(
|
||||
|
||||
@@ -7,14 +7,17 @@ namespace Daybreak.Linux.Services.Wine;
|
||||
public interface IWinePidMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a Wine-internal PID to a Linux system PID.
|
||||
/// Scans /proc for a process whose cmdline contains the executable name
|
||||
/// and whose environment references our Wine prefix.
|
||||
/// Converts a Wine-internal PID to a Linux system PID by scanning /proc.
|
||||
/// When <paramref name="executable"/> is a full path it is matched against each
|
||||
/// candidate's full executable path, which uniquely identifies the process even
|
||||
/// when multiple Guild Wars instances (different install directories) run at once.
|
||||
/// When only a file name is supplied, the first matching process is returned.
|
||||
/// </summary>
|
||||
/// <param name="winePid">The Wine-internal process ID (unused for lookup, kept for logging).</param>
|
||||
/// <param name="executableName">The executable name (e.g. "Gw.exe") to search for in /proc.</param>
|
||||
/// <param name="executable">The executable to match: a full Linux path (preferred, for
|
||||
/// unambiguous matching) or a bare file name (e.g. "Gw.exe") as a best-effort fallback.</param>
|
||||
/// <returns>The Linux PID, or null if not found.</returns>
|
||||
int? WinePidToLinuxPid(int winePid, string executableName);
|
||||
int? WinePidToLinuxPid(int winePid, string executable);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Linux system PID back to a Wine-internal PID.
|
||||
|
||||
@@ -17,8 +17,12 @@ public sealed class WinePidMapper(
|
||||
private readonly ILogger<WinePidMapper> logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? WinePidToLinuxPid(int winePid, string executableName)
|
||||
public int? WinePidToLinuxPid(int winePid, string executable)
|
||||
{
|
||||
var targetFileName = Path.GetFileName(executable);
|
||||
var matchByFullPath = executable.Contains('/') || executable.Contains('\\');
|
||||
var targetFullPath = matchByFullPath ? TryGetFullPath(executable) : null;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var procDir in Directory.EnumerateDirectories("/proc"))
|
||||
@@ -38,14 +42,35 @@ public sealed class WinePidMapper(
|
||||
}
|
||||
|
||||
var cmdline = File.ReadAllText(cmdlinePath);
|
||||
if (!cmdline.Contains(executableName, StringComparison.OrdinalIgnoreCase))
|
||||
if (cmdline.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// cmdline is null-separated; find the segment pointing at the executable.
|
||||
var segments = cmdline.Split('\0', StringSplitOptions.RemoveEmptyEntries);
|
||||
var exeSegment = segments.FirstOrDefault(s =>
|
||||
s.EndsWith(targetFileName, StringComparison.OrdinalIgnoreCase));
|
||||
if (exeSegment is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// When a full path was supplied, require an exact path match so that
|
||||
// concurrent Guild Wars instances in different directories are not confused.
|
||||
if (matchByFullPath)
|
||||
{
|
||||
var candidateFullPath = TryGetFullPath(WinePathToLinuxPath(exeSegment));
|
||||
if (candidateFullPath is null ||
|
||||
!string.Equals(candidateFullPath, targetFullPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.LogDebug(
|
||||
"Resolved Wine PID {WinePid} -> Linux PID {LinuxPid} for {ExeName}",
|
||||
winePid, pid, executableName
|
||||
"Resolved Wine PID {WinePid} -> Linux PID {LinuxPid} for {Executable}",
|
||||
winePid, pid, executable
|
||||
);
|
||||
|
||||
return pid;
|
||||
@@ -56,13 +81,39 @@ public sealed class WinePidMapper(
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.LogError(ex, "Error scanning /proc for Wine process {ExeName}", executableName);
|
||||
this.logger.LogError(ex, "Error scanning /proc for Wine process {Executable}", executable);
|
||||
}
|
||||
|
||||
this.logger.LogWarning("Could not find Linux PID for Wine PID {WinePid} ({ExeName})", winePid, executableName);
|
||||
this.logger.LogWarning("Could not find Linux PID for Wine PID {WinePid} ({Executable})", winePid, executable);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? TryGetFullPath(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Path.GetFullPath(path);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Wine Z: drive path back to a Linux path.
|
||||
/// "Z:\home\user\Guild Wars\Gw.exe" -> "/home/user/Guild Wars/Gw.exe".
|
||||
/// </summary>
|
||||
private static string WinePathToLinuxPath(string winePath)
|
||||
{
|
||||
if (winePath.StartsWith("Z:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
winePath = winePath[2..];
|
||||
}
|
||||
|
||||
return winePath.Replace('\\', '/');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? LinuxPidToWinePid(int linuxPid)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ public interface IPidProvider
|
||||
/// </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>
|
||||
/// <param name="port">The local TCP port owned by the process, when available.</param>
|
||||
/// <returns>The system process ID, or the original PID if conversion fails or is not needed.</returns>
|
||||
int ResolveSystemPid(int reportedPid, string executableName);
|
||||
int ResolveSystemPid(int reportedPid, string executableName, int? port = null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
using System.Diagnostics;
|
||||
using Daybreak.Configuration.Options;
|
||||
using Daybreak.Services.Toolbox;
|
||||
using Daybreak.Services.Toolbox.Utilities;
|
||||
using Daybreak.Shared.Exceptions;
|
||||
using Daybreak.Shared.Models;
|
||||
using Daybreak.Shared.Models.Mods;
|
||||
using Daybreak.Shared.Services.BuildTemplates;
|
||||
using Daybreak.Shared.Services.Injection;
|
||||
using Daybreak.Shared.Services.Notifications;
|
||||
using Daybreak.Shared.Services.Options;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Daybreak.Tests.Services.Toolbox;
|
||||
|
||||
/// <summary>
|
||||
/// Guards the mod lifecycle phase in which GWToolbox is injected. Toolbox must inject during
|
||||
/// <see cref="ToolboxService.OnGuildWarsCreated"/> (before Daybreak.API) and must NOT inject
|
||||
/// during <see cref="ToolboxService.OnGuildWarsStarted"/>. This ordering is what lets Toolbox
|
||||
/// load its bundled gwca.dll first so Daybreak.API reuses the same module.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public sealed class ToolboxServiceTests
|
||||
{
|
||||
private readonly IOptionsProvider optionsProvider = Substitute.For<IOptionsProvider>();
|
||||
private readonly IBuildTemplateManager buildTemplateManager = Substitute.For<IBuildTemplateManager>();
|
||||
private readonly INotificationService notificationService = Substitute.For<INotificationService>();
|
||||
private readonly IProcessInjector processInjector = Substitute.For<IProcessInjector>();
|
||||
private readonly IToolboxClient toolboxClient = Substitute.For<IToolboxClient>();
|
||||
private readonly IOptionsMonitor<ToolboxOptions> toolboxOptions = Substitute.For<IOptionsMonitor<ToolboxOptions>>();
|
||||
private readonly ToolboxOptions options = new();
|
||||
private readonly Process process = new();
|
||||
private readonly ToolboxService service;
|
||||
|
||||
public ToolboxServiceTests()
|
||||
{
|
||||
this.toolboxOptions.CurrentValue.Returns(this.options);
|
||||
this.service = new ToolboxService(
|
||||
this.optionsProvider,
|
||||
this.buildTemplateManager,
|
||||
this.notificationService,
|
||||
this.processInjector,
|
||||
this.toolboxClient,
|
||||
this.toolboxOptions,
|
||||
Substitute.For<ILogger<ToolboxService>>());
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup() => this.process.Dispose();
|
||||
|
||||
private GuildWarsCreatedContext CreatedContext() => new()
|
||||
{
|
||||
ApplicationLauncherContext = this.LauncherContext(),
|
||||
};
|
||||
|
||||
private GuildWarsStartedContext StartedContext() => new()
|
||||
{
|
||||
ApplicationLauncherContext = this.LauncherContext(),
|
||||
};
|
||||
|
||||
private ApplicationLauncherContext LauncherContext() => new()
|
||||
{
|
||||
ExecutablePath = "Gw.exe",
|
||||
Process = this.process,
|
||||
ProcessId = 1234,
|
||||
};
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnGuildWarsStarted_WhenEnabled_DoesNotInjectToolbox()
|
||||
{
|
||||
this.options.Enabled = true;
|
||||
var reachedLaunch = false;
|
||||
this.processInjector
|
||||
.Inject(Arg.Any<Process>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(_ => { reachedLaunch = true; return Task.FromResult(true); });
|
||||
|
||||
// OnGuildWarsStarted must be a pure no-op now that injection moved to OnGuildWarsCreated.
|
||||
await this.service.OnGuildWarsStarted(this.StartedContext(), CancellationToken.None);
|
||||
|
||||
reachedLaunch.Should().BeFalse("Toolbox injection moved out of OnGuildWarsStarted");
|
||||
await this.processInjector
|
||||
.DidNotReceive()
|
||||
.Inject(Arg.Any<Process>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnGuildWarsStarted_WhenDisabled_DoesNotInjectToolbox()
|
||||
{
|
||||
this.options.Enabled = false;
|
||||
|
||||
await this.service.OnGuildWarsStarted(this.StartedContext(), CancellationToken.None);
|
||||
|
||||
await this.processInjector
|
||||
.DidNotReceive()
|
||||
.Inject(Arg.Any<Process>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnGuildWarsCreated_WhenDisabled_DoesNotInjectToolbox()
|
||||
{
|
||||
this.options.Enabled = false;
|
||||
|
||||
await this.service.OnGuildWarsCreated(this.CreatedContext(), CancellationToken.None);
|
||||
|
||||
await this.processInjector
|
||||
.DidNotReceive()
|
||||
.Inject(Arg.Any<Process>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OnGuildWarsCreated_WhenEnabled_DrivesToolboxLaunch()
|
||||
{
|
||||
this.options.Enabled = true;
|
||||
var reachedLaunch = false;
|
||||
this.processInjector
|
||||
.Inject(Arg.Any<Process>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(_ => { reachedLaunch = true; return Task.FromResult(true); });
|
||||
|
||||
try
|
||||
{
|
||||
await this.service.OnGuildWarsCreated(this.CreatedContext(), CancellationToken.None);
|
||||
}
|
||||
catch (ExecutableNotFoundException)
|
||||
{
|
||||
// Reached LaunchToolbox, but the GWToolbox dll is not installed in this environment.
|
||||
// This still proves OnGuildWarsCreated routes into the toolbox launch path.
|
||||
reachedLaunch = true;
|
||||
}
|
||||
|
||||
reachedLaunch.Should().BeTrue("OnGuildWarsCreated must inject Toolbox before Daybreak.API");
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Daybreak.Windows.Services.Api;
|
||||
public sealed class PidProvider : IPidProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ResolveSystemPid(int reportedPid, string executableName)
|
||||
public int ResolveSystemPid(int reportedPid, string executableName, int? port = null)
|
||||
{
|
||||
// On Windows, the reported PID is the actual system PID
|
||||
return reportedPid;
|
||||
|
||||
Reference in New Issue
Block a user