using System.Diagnostics;
using System.Extensions.Core;
using Daybreak.Linux.Services.Wine;
using Daybreak.Shared.Models.LaunchConfigurations;
using Daybreak.Shared.Services.ApplicationLauncher;
using Microsoft.Extensions.Logging;
namespace Daybreak.Linux.Services.ApplicationLauncher;
///
/// Linux-specific Guild Wars process finder.
/// Scans /proc for Wine-hosted GW.exe processes in our Wine prefix,
/// then matches them to launch configurations by executable path
/// read from the process command line.
///
public sealed class GuildWarsProcessFinder(
IWinePrefixManager winePrefixManager,
ILogger logger
) : IGuildWarsProcessFinder
{
private const string GuildWarsExeName = "Gw.exe";
private readonly Memory guildWarsProcesses = new(new Process[256]);
private readonly IWinePrefixManager winePrefixManager = winePrefixManager;
private readonly ILogger logger = logger;
public Memory GetGuildWarsProcesses()
{
var scopedLogger = this.logger.CreateScopedLogger();
var index = 0;
foreach (var (pid, _) in this.ScanForGuildWarsProcesses())
{
try
{
this.guildWarsProcesses.Span[index] = Process.GetProcessById(pid);
index++;
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "Could not get process for PID {Pid}", pid);
}
}
scopedLogger.LogDebug("Found {Count} Guild Wars process(es) under Wine", index);
return this.guildWarsProcesses[..index];
}
public GuildWarsApplicationLaunchContext? FindProcess(
LaunchConfigurationWithCredentials configuration
)
{
return this.FindProcesses(configuration).FirstOrDefault();
}
///
/// Terminates the Wine-hosted Guild Wars process tree.
/// The process was already validated as a Gw.exe Wine process during discovery,
/// so we kill it directly. On Linux the process exposed by the context is the
/// Wine loader hosting Gw.exe; its therefore
/// points at the Wine loader rather than Gw.exe, which is why platform-agnostic
/// name checks cannot be used here.
///
public void KillProcess(GuildWarsApplicationLaunchContext guildWarsApplicationLaunchContext)
{
guildWarsApplicationLaunchContext.GuildWarsProcess.Kill(entireProcessTree: true);
}
public IEnumerable FindProcesses(
params LaunchConfigurationWithCredentials[] configurations
)
{
var scopedLogger = this.logger.CreateScopedLogger();
foreach (var (pid, executablePath) in this.ScanForGuildWarsProcesses())
{
// Match executable path from Wine cmdline to configuration
var matchedConfig = configurations.FirstOrDefault(c =>
ConfigurationMatchesPath(c, executablePath)
);
if (matchedConfig is null)
{
continue;
}
Process process;
try
{
process = Process.GetProcessById(pid);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "Could not get process for PID {Pid}", pid);
continue;
}
yield return new GuildWarsApplicationLaunchContext
{
GuildWarsProcess = process,
LaunchConfiguration = matchedConfig,
ProcessId = (uint)pid,
};
}
}
///
/// Scans /proc for processes running Gw.exe under our Wine prefix.
/// Returns tuples of (Linux PID, executable path from cmdline).
///
private IEnumerable<(int Pid, string ExecutablePath)> ScanForGuildWarsProcesses()
{
var prefixPath = this.winePrefixManager.GetWinePrefixPath();
foreach (var procDir in Directory.EnumerateDirectories("/proc"))
{
var dirName = Path.GetFileName(procDir);
if (!int.TryParse(dirName, out var pid))
{
continue;
}
string? executablePath = null;
try
{
var cmdlinePath = Path.Combine(procDir, "cmdline");
if (!File.Exists(cmdlinePath))
{
continue;
}
var cmdline = File.ReadAllText(cmdlinePath);
if (!cmdline.Contains(GuildWarsExeName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
// Extract the executable path from cmdline
// cmdline is null-separated; find the segment containing Gw.exe
var segments = cmdline.Split('\0', StringSplitOptions.RemoveEmptyEntries);
var exeSegment = segments.FirstOrDefault(s =>
s.EndsWith(GuildWarsExeName, StringComparison.OrdinalIgnoreCase)
);
if (exeSegment is not null)
{
// Convert Wine path (Z:\mnt\...\Gw.exe) to Linux path
executablePath = WinePathToLinuxPath(exeSegment);
}
}
catch (UnauthorizedAccessException)
{
continue;
}
catch (IOException)
{
continue;
}
if (executablePath is not null)
{
yield return (pid, executablePath);
}
}
}
///
/// Converts a Wine Z: path back to a Linux path.
/// "Z:\mnt\seagate\Games\Guild Wars\Gw.exe" -> "/mnt/seagate/Games/Guild Wars/Gw.exe"
///
private static string WinePathToLinuxPath(string winePath)
{
// Strip Z: prefix and convert backslashes
if (
winePath.StartsWith("Z:", StringComparison.OrdinalIgnoreCase)
|| winePath.StartsWith("z:", StringComparison.OrdinalIgnoreCase)
)
{
winePath = winePath[2..];
}
return winePath.Replace('\\', '/');
}
private static bool ConfigurationMatchesPath(
LaunchConfigurationWithCredentials configuration,
string executablePath
)
{
if (configuration.ExecutablePath is null)
{
return false;
}
return string.Equals(
Path.GetFullPath(configuration.ExecutablePath),
Path.GetFullPath(executablePath),
StringComparison.OrdinalIgnoreCase
);
}
}