Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compatibility rework #1348

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4a3f2d6
First successful compile and launch. May need tweaking
rankynbass Jun 17, 2023
6b698e3
Did a better LD_PRELOAD merge
rankynbass Jun 17, 2023
6e29c1d
Removed old constants from compatibilitytools
rankynbass Jun 17, 2023
4d62a15
Move more logic to runners
rankynbass Jun 18, 2023
99f8d75
More work done
rankynbass Jun 22, 2023
0634482
Move WineRunner, DxvkRunner to FFQL repo
rankynbass Jun 24, 2023
b0a8bd3
Add unpatched wine fix
rankynbass Jun 24, 2023
c37927c
Move MacVideoFix to xlcore
rankynbass Jun 24, 2023
d8e66ce
Fixed wrong folder in DxvkRunner.cs
rankynbass Jun 25, 2023
78e02be
Revert "Move MacVideoFix to xlcore"
rankynbass Jun 25, 2023
5c02352
Merge branch 'master' into compatibility-rework
rankynbass Jun 25, 2023
c6353d5
Implemented a bunch of stuff suggested by marzent:
rankynbass Jun 28, 2023
9284ecb
Forgot to fix Environment assignment in Settings classes
rankynbass Jun 28, 2023
6b73619
Fixed broken logic for finding wine or wine64.
rankynbass Jun 29, 2023
4624e0a
Fixed missing !
rankynbass Jun 29, 2023
22b5b08
Clean up the Ensure() functions, moved duplicate
rankynbass Jun 30, 2023
3076a12
Fix logic in GetUnixProcessId(), error handling of UnixDalamudRunner
rankynbass Jul 2, 2023
a16e3b1
Add workaround for REST & proton wine
rankynbass Jul 6, 2023
c07806f
No need for winedbg command if winePid is 0.
rankynbass Jul 6, 2023
ffef16d
Re-added global LD_PRELOAD so it gets merged.
rankynbass Jul 8, 2023
11cc916
Code cleanup per marzents coments
rankynbass Jul 23, 2023
1f697f3
Merge in marzent@f50acf4, remove [DALAMUD] in console output
rankynbass Jul 23, 2023
e7794d3
Get rid of unnecessary LD_PRELOAD merge.
rankynbass Jul 23, 2023
ec316fe
Rename some vars to make their purpose more apparent.
rankynbass Jul 23, 2023
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
267 changes: 166 additions & 101 deletions src/XIVLauncher.Common.Unix/Compatibility/CompatibilityTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,97 +17,117 @@ namespace XIVLauncher.Common.Unix.Compatibility;

public class CompatibilityTools
{
private DirectoryInfo toolDirectory;
private DirectoryInfo wineDirectory;

private DirectoryInfo dxvkDirectory;

private StreamWriter logWriter;

#if WINE_XIV_ARCH_LINUX
private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/8.5.r4.g4211bac7/wine-xiv-staging-fsync-git-arch-8.5.r4.g4211bac7.tar.xz";
#elif WINE_XIV_FEDORA_LINUX
private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/8.5.r4.g4211bac7/wine-xiv-staging-fsync-git-fedora-8.5.r4.g4211bac7.tar.xz";
#else
private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/8.5.r4.g4211bac7/wine-xiv-staging-fsync-git-ubuntu-8.5.r4.g4211bac7.tar.xz";
#endif
private const string WINE_XIV_RELEASE_NAME = "wine-xiv-staging-fsync-git-8.5.r4.g4211bac7";

public bool IsToolReady { get; private set; }

public WineSettings Settings { get; private set; }
public WineSettings WineSettings { get; private set; }

public DxvkSettings DxvkSettings { get; private set; }

private string WineBinPath => Settings.StartupType == WineStartupType.Managed ?
Path.Combine(toolDirectory.FullName, WINE_XIV_RELEASE_NAME, "bin") :
Settings.CustomBinPath;
private string Wine64Path => Path.Combine(WineBinPath, "wine64");
private string WineServerPath => Path.Combine(WineBinPath, "wineserver");
private string WineDLLOverrides;

public bool IsToolDownloaded => File.Exists(Wine64Path) && Settings.Prefix.Exists;
private FileInfo LogFile;

private readonly Dxvk.DxvkHudType hudType;
private readonly bool gamemodeOn;
private readonly string dxvkAsyncOn;
public DirectoryInfo Prefix { get; private set; }

public CompatibilityTools(WineSettings wineSettings, Dxvk.DxvkHudType hudType, bool? gamemodeOn, bool? dxvkAsyncOn, DirectoryInfo toolsFolder)
public bool IsToolDownloaded => File.Exists(WineSettings.RunCommand) && Prefix.Exists;

public bool IsFlatpak; // Not currently used.


public CompatibilityTools(WineSettings wineSettings, DxvkSettings dxvkSettings, DirectoryInfo prefix, DirectoryInfo toolsFolder, FileInfo logfile, bool isFlatpak)
{
this.Settings = wineSettings;
this.hudType = hudType;
this.gamemodeOn = gamemodeOn ?? false;
this.dxvkAsyncOn = (dxvkAsyncOn ?? false) ? "1" : "0";
WineSettings = wineSettings;
DxvkSettings = dxvkSettings;
Prefix = prefix;
WineDLLOverrides = $"msquic=,mscoree=n,b;d3d9,d3d11,d3d10core,dxgi={(dxvkSettings.Enabled ? "n,b" : "b")}";
wineDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "wine"));
dxvkDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "dxvk"));
LogFile = logfile;

this.toolDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "beta"));
this.dxvkDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "dxvk"));
logWriter = new StreamWriter(LogFile.FullName);

this.logWriter = new StreamWriter(wineSettings.LogFile.FullName);
IsFlatpak = isFlatpak;

if (wineSettings.StartupType == WineStartupType.Managed)
{
if (!this.toolDirectory.Exists)
this.toolDirectory.Create();
if (!wineDirectory.Exists)
wineDirectory.Create();

if (!this.dxvkDirectory.Exists)
this.dxvkDirectory.Create();
}
if (!dxvkDirectory.Exists)
dxvkDirectory.Create();

if (!wineSettings.Prefix.Exists)
wineSettings.Prefix.Create();
if (!Prefix.Exists)
Prefix.Create();
}

public async Task EnsureTool(DirectoryInfo tempPath)
{
if (!File.Exists(Wine64Path))
{
Log.Information("Compatibility tool does not exist, downloading");
await DownloadTool(tempPath).ConfigureAwait(false);
}

// Check to make sure wine is valid
await EnsureWine();
if (!File.Exists(WineSettings.RunCommand))
throw new FileNotFoundException("No wine or wine64 binary could be found.");
EnsurePrefix();
await Dxvk.InstallDxvk(Settings.Prefix, dxvkDirectory).ConfigureAwait(false);

// Check to make sure dxvk is valid
if (DxvkSettings.Enabled)
await EnsureDxvk();

IsToolReady = true;
Log.Information($"Using wine at path {WineSettings.RunCommand}");
}

private async Task EnsureWine()
{
if (!WineSettings.IsManaged) return;

await DownloadTool(wineDirectory.FullName, WineSettings.Folder, WineSettings.DownloadUrl);

// Use wine if wine64 isn't found. This is mostly for WoW64 wine builds.
}

private async Task DownloadTool(DirectoryInfo tempPath)
private async Task EnsureDxvk()
{
using var client = new HttpClient();
var tempFilePath = Path.Combine(tempPath.FullName, $"{Guid.NewGuid()}");
await DownloadTool(dxvkDirectory.FullName, DxvkSettings.Folder, DxvkSettings.DownloadUrl);

var prefixinstall = Path.Combine(Prefix.FullName, "drive_c", "windows", "system32");
var files = new DirectoryInfo(Path.Combine(dxvkDirectory.FullName, DxvkSettings.Folder, "x64")).GetFiles();

await File.WriteAllBytesAsync(tempFilePath, await client.GetByteArrayAsync(WINE_XIV_RELEASE_URL).ConfigureAwait(false)).ConfigureAwait(false);
foreach (FileInfo fileName in files)
fileName.CopyTo(Path.Combine(prefixinstall, fileName.Name), true);
}

PlatformHelpers.Untar(tempFilePath, this.toolDirectory.FullName);
private async Task DownloadTool(string toolDirectory, string toolFolder, string downloadUrl)
{
if (IsDirectoryEmpty(Path.Combine(toolDirectory, toolFolder)))
{
if (string.IsNullOrEmpty(downloadUrl))
{
Log.Error($"Attempted to download {toolFolder} without a download URL.");
throw new InvalidOperationException($"{toolFolder} does not exist, and no download URL was provided for it.");
}
Log.Information($"{toolFolder} does not exist. Downloading...");
using var client = new HttpClient();
var tempPath = Path.GetTempFileName();

Log.Information("Compatibility tool successfully extracted to {Path}", this.toolDirectory.FullName);
File.WriteAllBytes(tempPath, await client.GetByteArrayAsync(downloadUrl));
PlatformHelpers.Untar(tempPath, toolDirectory);

File.Delete(tempFilePath);
File.Delete(tempPath);
}
}

private void ResetPrefix()
{
Settings.Prefix.Refresh();
Prefix.Refresh();

if (Settings.Prefix.Exists)
Settings.Prefix.Delete(true);
if (Prefix.Exists)
Prefix.Delete(true);

Settings.Prefix.Create();
Prefix.Create();
EnsurePrefix();
}

Expand All @@ -118,86 +138,85 @@ public void EnsurePrefix()

public Process RunInPrefix(string command, string workingDirectory = "", IDictionary<string, string> environment = null, bool redirectOutput = false, bool writeLog = false, bool wineD3D = false)
{
var psi = new ProcessStartInfo(Wine64Path);
psi.Arguments = command;
var psi = new ProcessStartInfo(WineSettings.RunCommand);
psi.Arguments = command.Trim();

Log.Verbose("Running in prefix: {FileName} {Arguments}", psi.FileName, command);
Log.Verbose("Running in prefix: {FileName} {Arguments}", psi.FileName, psi.Arguments);
return RunInPrefix(psi, workingDirectory, environment, redirectOutput, writeLog, wineD3D);
}

public Process RunInPrefix(string[] args, string workingDirectory = "", IDictionary<string, string> environment = null, bool redirectOutput = false, bool writeLog = false, bool wineD3D = false)
{
var psi = new ProcessStartInfo(Wine64Path);
var psi = new ProcessStartInfo(WineSettings.RunCommand);
foreach (var arg in args)
psi.ArgumentList.Add(arg);

Log.Verbose("Running in prefix: {FileName} {Arguments}", psi.FileName, psi.ArgumentList.Aggregate(string.Empty, (a, b) => a + " " + b));
return RunInPrefix(psi, workingDirectory, environment, redirectOutput, writeLog, wineD3D);
}

private void MergeDictionaries(StringDictionary a, IDictionary<string, string> b)
private void MergeDictionaries(IDictionary<string, string> a, IDictionary<string, string> b)
{
if (b is null)
return;

foreach (var keyValuePair in b)
{
if (a.ContainsKey(keyValuePair.Key))
a[keyValuePair.Key] = keyValuePair.Value;
{
if (keyValuePair.Key == "LD_PRELOAD")
a[keyValuePair.Key] = MergeLDPreload(a[keyValuePair.Key], keyValuePair.Value);
else
a[keyValuePair.Key] = keyValuePair.Value;
}
else
a.Add(keyValuePair.Key, keyValuePair.Value);
}
}

private string MergeLDPreload(string a, string b)
{
var alist = a.Split(':');
var blist = b.Split(':');

var merged = alist.Union(blist);

var ldpreload = "";
foreach (var item in merged)
ldpreload += item + ":";

return ldpreload.TrimEnd(':');
}

private Process RunInPrefix(ProcessStartInfo psi, string workingDirectory, IDictionary<string, string> environment, bool redirectOutput, bool writeLog, bool wineD3D)
{
psi.RedirectStandardOutput = redirectOutput;
psi.RedirectStandardError = writeLog;
psi.UseShellExecute = false;
psi.WorkingDirectory = workingDirectory;

var wineEnviromentVariables = new Dictionary<string, string>();
wineEnviromentVariables.Add("WINEPREFIX", Settings.Prefix.FullName);
wineEnviromentVariables.Add("WINEDLLOVERRIDES", $"msquic=,mscoree=n,b;d3d9,d3d11,d3d10core,dxgi={(wineD3D ? "b" : "n")}");

if (!string.IsNullOrEmpty(Settings.DebugVars))
{
wineEnviromentVariables.Add("WINEDEBUG", Settings.DebugVars);
}

wineEnviromentVariables.Add("XL_WINEONLINUX", "true");
string ldPreload = Environment.GetEnvironmentVariable("LD_PRELOAD") ?? "";

string dxvkHud = hudType switch
{
Dxvk.DxvkHudType.None => "0",
Dxvk.DxvkHudType.Fps => "fps",
Dxvk.DxvkHudType.Full => "full",
_ => throw new ArgumentOutOfRangeException()
};

if (this.gamemodeOn == true && !ldPreload.Contains("libgamemodeauto.so.0"))
{
ldPreload = ldPreload.Equals("") ? "libgamemodeauto.so.0" : ldPreload + ":libgamemodeauto.so.0";
}

wineEnviromentVariables.Add("DXVK_HUD", dxvkHud);
wineEnviromentVariables.Add("DXVK_ASYNC", dxvkAsyncOn);
wineEnviromentVariables.Add("WINEESYNC", Settings.EsyncOn);
wineEnviromentVariables.Add("WINEFSYNC", Settings.FsyncOn);

wineEnviromentVariables.Add("LD_PRELOAD", ldPreload);

MergeDictionaries(psi.EnvironmentVariables, wineEnviromentVariables);
MergeDictionaries(psi.EnvironmentVariables, environment);
var wineEnvironmentVariables = new Dictionary<string, string>();
if (wineD3D)
wineEnvironmentVariables.Add("WINEDLLOVERRIDES", "msquic=,mscoree=n,b;d3d9,d3d11,d3d10core,dxgi=b");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here is completely different from WineDLLOverrides above and should be consolidated (ideally here)

else
wineEnvironmentVariables.Add("WINEDLLOVERRIDES", WineDLLOverrides);
wineEnvironmentVariables.Add("XL_WINEONLINUX", "true");
var ldPreload = Environment.GetEnvironmentVariable("LD_PRELOAD");
if (ldPreload is not null)
wineEnvironmentVariables.Add("LD_PRELOAD", ldPreload);

MergeDictionaries(psi.Environment, WineSettings.Environment);
MergeDictionaries(psi.Environment, DxvkSettings.Environment);
MergeDictionaries(psi.Environment, wineEnvironmentVariables);
MergeDictionaries(psi.Environment, environment);

#if FLATPAK_NOTRIGHTNOW
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the corresponding xlcore PR FLATPAK constants are removed but they are still here, would be nice to have a unified flatpack detection/handling logic (also FLATPAK_NOTRIGHTNOW is not really used at all currently)

psi.FileName = "flatpak-spawn";

psi.ArgumentList.Insert(0, "--host");
psi.ArgumentList.Insert(1, Wine64Path);
psi.ArgumentList.Insert(1, WineSettings.RunCommand);

foreach (KeyValuePair<string, string> envVar in wineEnviromentVariables)
foreach (KeyValuePair<string, string> envVar in wineEnvironmentVariables)
{
psi.ArgumentList.Insert(1, $"--env={envVar.Key}={envVar.Value}");
}
Expand Down Expand Up @@ -258,13 +277,53 @@ public Int32 GetUnixProcessId(Int32 winePid)
var wineDbg = RunInPrefix("winedbg --command \"info procmap\"", redirectOutput: true);
var output = wineDbg.StandardOutput.ReadToEnd();
if (output.Contains("syntax error\n"))
return 0;
{
var processName = GetProcessName(winePid);
return GetUnixProcessIdByName(processName);
}
var matchingLines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Skip(1).Where(
l => int.Parse(l.Substring(1, 8), System.Globalization.NumberStyles.HexNumber) == winePid);
var unixPids = matchingLines.Select(l => int.Parse(l.Substring(10, 8), System.Globalization.NumberStyles.HexNumber)).ToArray();
return unixPids.FirstOrDefault();
}

private string GetProcessName(Int32 winePid)
{
var wineDbg = RunInPrefix("winedbg --command \"info proc\"", redirectOutput: true);
var output = wineDbg.StandardOutput.ReadToEnd();
var matchingLines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Skip(1).Where(
l => int.Parse(l.Substring(1, 8), System.Globalization.NumberStyles.HexNumber) == winePid);
var processNames = matchingLines.Select( l => l.Substring(20).Trim('\'')).ToArray();
return processNames.FirstOrDefault();
}

private Int32 GetUnixProcessIdByName(string executableName)
{
int closest = 0;
int early = 0;
var currentProcess = Process.GetCurrentProcess(); // Gets XIVLauncher.Core's process
bool nonunique = false;
foreach (var process in Process.GetProcessesByName(executableName))
{
if (process.Id < currentProcess.Id)
{
early = process.Id;
continue; // Process was launched before XIVLauncher.Core
}
// Assume that the closest PID to XIVLauncher.Core's is the correct one. But log an error if more than one is found.
if ((closest - currentProcess.Id) > (process.Id - currentProcess.Id) || closest == 0)
{
if (closest != 0) nonunique = true;
closest = process.Id;
}
if (nonunique) Log.Error($"More than one {executableName} found! Selecting the most likely match with process id {closest}.");
}
// Deal with rare edge-case where pid rollover causes the ffxiv pid to be lower than XLCore's.
if (closest == 0 && early != 0) closest = early;
if (closest != 0) Log.Verbose($"Process for {executableName} found using fallback method: {closest}. XLCore pid: {currentProcess.Id}");
return closest;
}

public string UnixToWinePath(string unixPath)
{
var launchArguments = new string[] { "winepath", "--windows", unixPath };
Expand All @@ -282,12 +341,18 @@ public void AddRegistryKey(string key, string value, string data)

public void Kill()
{
var psi = new ProcessStartInfo(WineServerPath)
var psi = new ProcessStartInfo(WineSettings.WineServer)
{
Arguments = "-k"
};
psi.EnvironmentVariables.Add("WINEPREFIX", Settings.Prefix.FullName);
psi.Environment.Add("WINEPREFIX", Prefix.FullName);

Process.Start(psi);
}

private bool IsDirectoryEmpty(string folder)
{
if (!Directory.Exists(folder)) return true;
return !Directory.EnumerateFileSystemEntries(folder).Any();
}
}
Loading