Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Daybreak.Injector/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ internal static partial class NativeMethods
[LibraryImport("kernel32.dll")]
public static partial nint OpenProcess(ProcessAccessFlags dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwProcessID);

[LibraryImport("kernel32.dll", EntryPoint = "QueryFullProcessImageNameW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool QueryFullProcessImageName(nint hProcess, uint dwFlags, [Out] char[] lpExeName, ref uint lpdwSize);

[LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleW", StringMarshalling = StringMarshalling.Utf16)]
public static partial nint GetModuleHandle(string lpModuleName);

Expand Down
78 changes: 78 additions & 0 deletions Daybreak.Injector/ProcessResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Daybreak.Shared.Models;
using System.Diagnostics;

namespace Daybreak.Injector;

/// <summary>
/// Resolves a Wine-internal process id from a Guild Wars executable path.
///
/// On Linux multiple Guild Wars instances (each in its own install directory) run under
/// the same Wine prefix. Matching by executable name alone cannot tell them apart, so we
/// enumerate the running processes and compare each one's full image path — which the
/// caller already knows from the Linux side — to find the unique owning Wine process.
///
/// This runs inside Wine, where <see cref="Process.Id"/> is the Wine pid, so the matched
/// id is exactly what the stub/winapi injectors expect.
/// </summary>
public static class ProcessResolver
{
private const string GuildWarsProcessName = "Gw";

public static InjectorResponses.ResolveResult Resolve(string executablePath, out int processId)
{
processId = 0;
var target = NormalizePath(executablePath);

foreach (var process in Process.GetProcessesByName(GuildWarsProcessName))
{
try
{
if (TryGetImagePath(process.Id) is { } imagePath &&
string.Equals(NormalizePath(imagePath), target, StringComparison.Ordinal))
{
processId = process.Id;
return InjectorResponses.ResolveResult.Success;
}
}
finally
{
process.Dispose();
}
}

return InjectorResponses.ResolveResult.ProcessNotFound;
}

private static string? TryGetImagePath(int processId)
{
var handle = NativeMethods.OpenProcess(
NativeMethods.ProcessAccessFlags.QueryLimitedInformation, false, (uint)processId);
if (handle is 0)
{
return null;
}

try
{
var buffer = new char[1024];
var size = (uint)buffer.Length;
if (!NativeMethods.QueryFullProcessImageName(handle, 0, buffer, ref size) || size is 0)
{
return null;
}

return new string(buffer, 0, (int)size);
}
finally
{
NativeMethods.CloseHandle(handle);
}
}

/// <summary>
/// Normalizes a Windows/Wine path for comparison: unifies separators, trims a trailing
/// null/whitespace, and lower-cases (Windows paths are case-insensitive).
/// </summary>
private static string NormalizePath(string path) =>
path.Trim().TrimEnd('\0').Replace('/', '\\').ToLowerInvariant();
}
38 changes: 38 additions & 0 deletions Daybreak.Injector/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ static void PrintUsage()
Console.WriteLine("- stub");
Console.WriteLine("- launch");
Console.WriteLine("- resume");
Console.WriteLine("- resolve");
Console.WriteLine("Examples:");
Console.WriteLine("1) Daybreak.Injector winapi 1234 C:\\path\\to\\dll.dll");
Console.WriteLine("2) Daybreak.Injector stub 1234 entryPoint C:\\path\\to\\dll.dll");
Console.WriteLine("3) Daybreak.Injector launch true C:\\path\\to\\dll.dll arg1 arg2 arg3");
Console.WriteLine("4) Daybreak.Injector resume 1234");
Console.WriteLine("5) Daybreak.Injector resolve \"Z:\\path\\to\\Gw.exe\"");
Console.WriteLine("======================================================");
}

Expand Down Expand Up @@ -173,6 +175,24 @@ static bool TryParseThreadResumeArgs(
return true;
}

static bool TryParseResolveArgs(
string[] args,
[NotNullWhen(true)] out string? executablePath,
out InjectorResponses.ResolveResult exitCode)
{
executablePath = default;
if (args.Length < 2 || string.IsNullOrWhiteSpace(args[1]))
{
PrintUsage();
exitCode = InjectorResponses.ResolveResult.InvalidArgs;
return false;
}

executablePath = args[1];
exitCode = InjectorResponses.ResolveResult.Success;
return true;
}

if (args.Length < 1)
{
PrintUsage();
Expand Down Expand Up @@ -244,6 +264,24 @@ static bool TryParseThreadResumeArgs(
Console.WriteLine($"ExitCode: {result}");
return result;
}
case "resolve":
{
if (!TryParseResolveArgs(args, out var executablePath, out var parseResult))
{
Console.WriteLine($"ExitCode: {(int)parseResult}");
return (int)parseResult;
}

Console.WriteLine($"Resolving Wine PID for {executablePath}");
var result = ProcessResolver.Resolve(executablePath, out var processId);
if (result is InjectorResponses.ResolveResult.Success)
{
Console.WriteLine($"ProcessId: {processId}");
}

Console.WriteLine($"ExitCode: {(int)result}");
return (int)result;
}
default:
PrintUsage();
Console.WriteLine($"ExitCode: {(int)InjectorResponses.GenericResults.InvalidMode}");
Expand Down
95 changes: 91 additions & 4 deletions Daybreak.Linux/Services/Injection/DaybreakInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ IWinePidMapper winePidMapper
) : IDaybreakInjector
{
private const string InjectorRelativePath = "Injector/Daybreak.Injector.exe";
private const string GuildWarsExecutableName = "Gw.exe";

private readonly ILogger<DaybreakInjector> logger = logger;
private readonly IWinePrefixManager winePrefixManager = winePrefixManager;
Expand Down Expand Up @@ -57,8 +58,9 @@ CancellationToken cancellationToken
return InjectorResponses.InjectResult.InvalidInjector;
}

// Convert Linux PID to Wine PID for the injector
var winePid = this.winePidMapper.LinuxPidToWinePid(processId);
// Resolve the Wine PID by matching the process's full image path (handles
// multiple concurrent Guild Wars instances).
var winePid = await this.ResolveWinePid(processId, cancellationToken);
if (winePid is null)
{
scopedLogger.LogError("No Wine PID mapping found for Linux PID {ProcessId}", processId);
Expand Down Expand Up @@ -99,8 +101,9 @@ CancellationToken cancellationToken
return InjectorResponses.InjectResult.InvalidInjector;
}

// Convert Linux PID to Wine PID for the injector
var winePid = this.winePidMapper.LinuxPidToWinePid(processId);
// Resolve the Wine PID by matching the process's full image path (handles
// multiple concurrent Guild Wars instances).
var winePid = await this.ResolveWinePid(processId, cancellationToken);
if (winePid is null)
{
scopedLogger.LogError("No Wine PID mapping found for Linux PID {ProcessId}", processId);
Expand Down Expand Up @@ -290,4 +293,88 @@ private static int ParseExitCodeFromOutput(string? output, int defaultExitCode)

return defaultExitCode;
}

/// <summary>
/// Resolves the Wine PID for a Linux Guild Wars process by asking the injector (which runs
/// inside Wine) to match on the process's full image path. This disambiguates multiple
/// concurrent Guild Wars instances, which a name-only lookup cannot.
/// </summary>
private async Task<int?> ResolveWinePid(int linuxPid, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();

var wineExecutablePath = TryGetWineExecutablePath(linuxPid);
if (wineExecutablePath is null)
{
scopedLogger.LogError("Could not read Wine executable path for Linux PID {ProcessId}", linuxPid);
return null;
}

var (output, _, _) = await this.LaunchInjector(
["resolve", $"\"{wineExecutablePath}\""],
cancellationToken,
completionChecker: (line, _) => line.StartsWith("ExitCode: ")
);

var winePid = ParseLabeledIntFromOutput(output, "ProcessId: ");
if (winePid is null)
{
scopedLogger.LogWarning(
"Injector could not resolve Wine PID for {ExecutablePath} (Linux PID {ProcessId})",
wineExecutablePath,
linuxPid
);
return null;
}

scopedLogger.LogDebug(
"Resolved Linux PID {ProcessId} -> Wine PID {WinePid} via {ExecutablePath}",
linuxPid,
winePid.Value,
wineExecutablePath
);
return winePid;
}

/// <summary>
/// Reads the Wine-form executable path (e.g. "Z:\home\...\Gw.exe") from the process's
/// command line, used to uniquely identify it among concurrent Guild Wars instances.
/// </summary>
private static string? TryGetWineExecutablePath(int linuxPid)
{
try
{
var cmdline = File.ReadAllText($"/proc/{linuxPid}/cmdline");
var segments = cmdline.Split('\0', StringSplitOptions.RemoveEmptyEntries);
return segments.FirstOrDefault(s =>
s.EndsWith(GuildWarsExecutableName, StringComparison.OrdinalIgnoreCase));
}
catch (IOException)
{
return null;
}
catch (UnauthorizedAccessException)
{
return null;
}
}

private static int? ParseLabeledIntFromOutput(string? output, string label)
{
if (output is null)
{
return null;
}

foreach (var line in output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries))
{
if (line.StartsWith(label) &&
int.TryParse(line[label.Length..], out var value))
{
return value;
}
}

return null;
}
}
14 changes: 3 additions & 11 deletions Daybreak.Linux/Services/Wine/IWinePidMapper.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
namespace Daybreak.Linux.Services.Wine;

/// <summary>
/// Stateless translator between Wine-internal PIDs and Linux system PIDs.
/// Uses /proc scanning and winedbg to resolve mappings on demand.
/// Translates Wine-internal PIDs to Linux system PIDs by scanning /proc.
/// The reverse direction (Linux → Wine PID) is handled by the injector, which runs
/// inside Wine and can disambiguate concurrent instances by full image path.
/// </summary>
public interface IWinePidMapper
{
Expand All @@ -18,13 +19,4 @@ public interface IWinePidMapper
/// 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 executable);

/// <summary>
/// Converts a Linux system PID back to a Wine-internal PID.
/// Reads the process's cmdline to determine the executable, then queries
/// winedbg to find the corresponding Wine PID.
/// </summary>
/// <param name="linuxPid">The Linux system PID.</param>
/// <returns>The Wine PID, or null if not found.</returns>
int? LinuxPidToWinePid(int linuxPid);
}
Loading