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 ); } }