Skip to content
Draft
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
10 changes: 8 additions & 2 deletions WheelWizard.Test/Features/LinuxDolphinInstallerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ public LinuxDolphinInstallerTests()
[Fact]
public void IsDolphinInstalledInFlatpak_ReturnsTrue_WhenFlatpakListHasDolphin()
{
_processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _).Returns(Ok(0)).AndDoes(callInfo => callInfo[2] = "Application ID\norg.DolphinEmu.dolphin-emu\n");
_processService
.Run("flatpak", "list --app --columns=application", out var stdOut, out _)
.Returns(Ok(0))
.AndDoes(callInfo => callInfo[2] = "Application ID\norg.DolphinEmu.dolphin-emu\n");

var result = _installer.IsDolphinInstalledInFlatpak();

Expand All @@ -29,7 +32,10 @@ public void IsDolphinInstalledInFlatpak_ReturnsTrue_WhenFlatpakListHasDolphin()
[Fact]
public void IsDolphinInstalledInFlatpak_ReturnsFalse_WhenFlatpakListHasNoDolphin()
{
_processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _).Returns(Ok(0)).AndDoes(callInfo => callInfo[2] = "Application ID\n");
_processService
.Run("flatpak", "list --app --columns=application", out var stdOut, out _)
.Returns(Ok(0))
.AndDoes(callInfo => callInfo[2] = "Application ID\n");

var result = _installer.IsDolphinInstalledInFlatpak();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ public bool IsDolphinInstalledInFlatpak()
const string dolphinAppId = "org.DolphinEmu.dolphin-emu";
var processResult = processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _);

return processResult.IsSuccess && processResult.Value == 0 && stdOut
.Split('\n')
.Any(line => line == dolphinAppId);
return processResult.IsSuccess && processResult.Value == 0 && stdOut.Split('\n').Any(line => line == dolphinAppId);
}

public bool IsDolphinInstalledNative()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace WheelWizard.DolphinManagement.Abstractions;

public record DolphinInstallation(string DisplayName, string LaunchTarget, bool Found);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace WheelWizard.DolphinManagement.Abstractions;

public interface IDolphinInstaller
{
//IReadOnlyList<DolphinInstallation> AvailableInstallationMethods();
Task<OperationResult> InstallDolphin(IProgress<int>? progress);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace WheelWizard.DolphinManagement.Abstractions;

public interface IDolphinLocator
{
IReadOnlyList<DolphinInstallation> DetectInstallations();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using WheelWizard.DolphinManagement.Abstractions;
using WheelWizard.DolphinManagement.Linux;

namespace WheelWizard.DolphinManagement;

public static class DolphinManagmentExtensions
{
public static IServiceCollection AddDolphinManagement(this IServiceCollection services)
{
#if LINUX
services.AddSingleton<ILinuxCommandEnvironment, LinuxCommandEnvironment>();
services.AddSingleton<ILinuxProcessService, LinuxProcessService>();
services.AddSingleton<IDolphinInstaller, LinuxDolphinInstaller>();
services.AddSingleton<IDolphinLocator, LinuxDolphinLocator>();
#endif
return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using WheelWizard.Helpers;

namespace WheelWizard.DolphinManagement.Linux;

public interface ILinuxCommandEnvironment
{
bool IsCommandAvailable(string command);
string DetectPackageManagerInstallCommand();
}

public sealed class LinuxCommandEnvironment : ILinuxCommandEnvironment
{
public bool IsCommandAvailable(string command)
{
return EnvHelper.IsValidUnixCommand(command);
}

public string DetectPackageManagerInstallCommand()
{
return EnvHelper.DetectLinuxPackageManagerInstallCommand();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using WheelWizard.DolphinManagement.Abstractions;

namespace WheelWizard.DolphinManagement.Linux;

public sealed class LinuxDolphinInstaller(ILinuxCommandEnvironment commandEnvironment, ILinuxProcessService processService)
: IDolphinInstaller
{
private bool IsFlatpakInstalled()
{
return commandEnvironment.IsCommandAvailable("flatpak");
}

private async Task<OperationResult> InstallFlatpak(IProgress<int>? progress = null)
{
if (IsFlatpakInstalled())
return Ok();

var packageManagerCommand = commandEnvironment.DetectPackageManagerInstallCommand();
if (string.IsNullOrWhiteSpace(packageManagerCommand))
return Fail("Unsupported Linux distribution. Could not detect a package manager command.");

var installResult = await processService.RunWithProgressAsync("pkexec", $"{packageManagerCommand} flatpak", progress);
if (installResult.IsFailure)
return installResult.Error;

if (installResult.Value is 126 or 127)
return Fail("You need to be an administrator to install Flatpak.");

if (installResult.Value != 0)
return Fail($"Flatpak installation failed with exit code {installResult.Value}.");

if (!IsFlatpakInstalled())
return Fail("Flatpak installation completed, but Flatpak is still unavailable.");

return Ok();
}

public async Task<OperationResult> InstallDolphin(IProgress<int>? progress = null)
{
if (!IsFlatpakInstalled())
{
var installFlatpakResult = await InstallFlatpak(progress);
if (installFlatpakResult.IsFailure)
return installFlatpakResult;
}

var addRemoteResult = processService.Run(
"flatpak",
"remote-add --if-not-exists --user dolphin https://flatpak.dolphin-emu.org/releases.flatpakrepo"
);
if (addRemoteResult.IsFailure)
return addRemoteResult.Error;

if (addRemoteResult.Value != 0)
return Fail($"Adding the Dolphin Flatpak remote failed with exit code {addRemoteResult.Value}.");

addRemoteResult = processService.Run(
"flatpak",
"remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo"
);
if (addRemoteResult.IsFailure)
return addRemoteResult.Error;

if (addRemoteResult.Value != 0)
return Fail($"Adding the Flathub Flatpak remote failed with exit code {addRemoteResult.Value}.");

var installDolphinResult = await processService.RunWithProgressAsync(
"flatpak",
"install --user -y dolphin org.DolphinEmu.dolphin-emu",
progress
);
if (installDolphinResult.IsFailure)
return installDolphinResult.Error;

if (installDolphinResult.Value != 0)
return Fail($"Dolphin installation failed with exit code {installDolphinResult.Value}.");

var launchResult = await processService.LaunchAndStopAsync("flatpak", "run org.DolphinEmu.dolphin-emu", TimeSpan.FromSeconds(4));
return launchResult.IsFailure ? launchResult.Error : Ok();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using WheelWizard.DolphinManagement.Abstractions;

namespace WheelWizard.DolphinManagement.Linux;

public sealed class LinuxDolphinLocator(ILinuxCommandEnvironment commandEnvironment, ILinuxProcessService processService) : IDolphinLocator
{
private bool IsDolphinInstalledInFlatpak()
{
const string dolphinAppId = "org.DolphinEmu.dolphin-emu";
var processResult = processService.Run("flatpak", "list --app --columns=application", out var stdOut, out _);

return processResult.IsSuccess && processResult.Value == 0 && stdOut.Split('\n').Any(line => line == dolphinAppId);
}

private bool IsDolphinInstalledNative()
{
if (!commandEnvironment.IsCommandAvailable("dolphin-emu"))
{
return false;
}
var processResult = processService.Run("dolphin-emu", "--version");
return processResult.IsSuccess && processResult.Value == 0;
}

public IReadOnlyList<DolphinInstallation> DetectInstallations()
{
return
[
new("Flatpak", "flatpak run org.DolphinEmu.dolphin-emu", IsDolphinInstalledInFlatpak()),
new("Native", "dolphin-emu", IsDolphinInstalledNative()),
];
}
}
123 changes: 123 additions & 0 deletions WheelWizard/Features/DolphinManagement/Linux/LinuxProcessService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace WheelWizard.DolphinManagement.Linux;

public interface ILinuxProcessService
{
OperationResult<int> Run(string fileName, string arguments, out string stdOut, out string stdErr);
OperationResult<int> Run(string fileName, string arguments);
Task<OperationResult<int>> RunWithProgressAsync(string fileName, string arguments, IProgress<int>? progress = null);
Task<OperationResult> LaunchAndStopAsync(string fileName, string arguments, TimeSpan duration);
}

public sealed class LinuxProcessService : ILinuxProcessService
{
public OperationResult<int> Run(string fileName, string arguments, out string stdOut, out string stdErr)
{
var localStdOut = "";
var localStdErr = "";
var result = TryCatch(
() =>
{
var processInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};

using var process = Process.Start(processInfo);
if (process == null)
return -1;

localStdOut = process.StandardOutput.ReadToEnd();
localStdErr = process.StandardError.ReadToEnd();
process.WaitForExit();
return process.ExitCode;
},
$"Failed to run process: {fileName} {arguments}"
);

stdOut = localStdOut;
stdErr = localStdErr;
return result;
}

public OperationResult<int> Run(string fileName, string arguments)
{
return Run(fileName, arguments, out _, out _);
}

public async Task<OperationResult<int>> RunWithProgressAsync(string fileName, string arguments, IProgress<int>? progress = null)
{
return await TryCatch(
async () =>
{
var processInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};

using var process = Process.Start(processInfo);
if (process == null)
return -1;

process.OutputDataReceived += (_, eventArgs) => ReportProgress(eventArgs.Data, progress);
process.ErrorDataReceived += (_, eventArgs) => ReportProgress(eventArgs.Data, progress);

process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync();
return process.ExitCode;
},
$"Failed to run process: {fileName} {arguments}"
);
}

public async Task<OperationResult> LaunchAndStopAsync(string fileName, string arguments, TimeSpan duration)
{
return await TryCatch(
async () =>
{
using var process = new Process
{
StartInfo = new()
{
FileName = fileName,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
},
};

process.Start();
await Task.Delay(duration);

if (!process.HasExited)
process.Kill();
},
$"Failed to run process: {fileName} {arguments}"
);
}

private static void ReportProgress(string? output, IProgress<int>? progress)
{
if (string.IsNullOrWhiteSpace(output))
return;

var match = Regex.Match(output, @"(\d+)%");
if (match.Success && int.TryParse(match.Groups[1].Value, out var percent))
progress?.Report(percent);
}
}
11 changes: 5 additions & 6 deletions WheelWizard/Features/Settings/SettingsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ public class SettingsManager : ISettingsManager
private double _internalScale = -1.0;

#region Constructor
public SettingsManager(
IWhWzSettingManager whWzSettingManager,
IDolphinSettingManager dolphinSettingManager,
IFileSystem fileSystem
)
public SettingsManager(IWhWzSettingManager whWzSettingManager, IDolphinSettingManager dolphinSettingManager, IFileSystem fileSystem)
{
_whWzSettingManager = whWzSettingManager;
_dolphinSettingManager = dolphinSettingManager;
Expand Down Expand Up @@ -90,7 +86,10 @@ IFileSystem fileSystem
return false;

// `~/.dolphin-emu` would be used if it exists
if (!PathManager.IsFlatpakDolphinFilePath(dolphinLocation) && _fileSystem.Directory.Exists(PathManager.LinuxDolphinLegacyFolderPath))
if (
!PathManager.IsFlatpakDolphinFilePath(dolphinLocation)
&& _fileSystem.Directory.Exists(PathManager.LinuxDolphinLegacyFolderPath)
)
return false;

return true;
Expand Down
Loading
Loading