fix(launcher): require executable Linux hosts
This commit is contained in:
parent
10a712d66b
commit
ae2cbbee8c
5 changed files with 134 additions and 10 deletions
7
.github/workflows/headless-portability.yml
vendored
7
.github/workflows/headless-portability.yml
vendored
|
|
@ -135,6 +135,13 @@ jobs:
|
|||
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- validate --config headless-k0.json
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
- name: Verify Linux headless host executable permission
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -x src/AcDream.Headless/bin/Release/net10.0/acdream-headless
|
||||
|
||||
portable-launcher:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
|
|
@ -12,12 +12,14 @@ namespace AcDream.Launcher.Core.Orchestration;
|
|||
public sealed class LauncherExecutableSet
|
||||
{
|
||||
private readonly Func<string, bool> _fileExists;
|
||||
private readonly Func<string, bool> _hasUnixExecutePermission;
|
||||
|
||||
public LauncherExecutableSet(
|
||||
string graphicalHostPath,
|
||||
string headlessHostPath,
|
||||
string? workingDirectory = null,
|
||||
Func<string, bool>? fileExists = null)
|
||||
Func<string, bool>? fileExists = null,
|
||||
Func<string, bool>? hasUnixExecutePermission = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(graphicalHostPath);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(headlessHostPath);
|
||||
|
|
@ -25,6 +27,8 @@ public sealed class LauncherExecutableSet
|
|||
HeadlessHostPath = headlessHostPath;
|
||||
WorkingDirectory = workingDirectory;
|
||||
_fileExists = fileExists ?? File.Exists;
|
||||
_hasUnixExecutePermission =
|
||||
hasUnixExecutePermission ?? HasUnixExecutePermission;
|
||||
}
|
||||
|
||||
public string GraphicalHostPath { get; }
|
||||
|
|
@ -38,17 +42,25 @@ public sealed class LauncherExecutableSet
|
|||
string path = mode == LaunchMode.Headless
|
||||
? HeadlessHostPath
|
||||
: GraphicalHostPath;
|
||||
if (_fileExists(path))
|
||||
{
|
||||
return LauncherCapability.Available;
|
||||
}
|
||||
|
||||
string host = mode == LaunchMode.Headless
|
||||
? "headless host"
|
||||
: "graphical client";
|
||||
return LauncherCapability.Unavailable(
|
||||
$"The co-deployed {host} is missing at '{path}'. Reinstall or update "
|
||||
+ "the client before launching.");
|
||||
if (!_fileExists(path))
|
||||
{
|
||||
return LauncherCapability.Unavailable(
|
||||
$"The co-deployed {host} is missing at '{path}'. Reinstall or update "
|
||||
+ "the client before launching.");
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux() && !_hasUnixExecutePermission(path))
|
||||
{
|
||||
return LauncherCapability.Unavailable(
|
||||
$"The co-deployed {host} at '{path}' exists but is not executable. "
|
||||
+ "Restore its executable permission (for example, chmod +x) or "
|
||||
+ "reinstall/update the client before launching.");
|
||||
}
|
||||
|
||||
return LauncherCapability.Available;
|
||||
}
|
||||
|
||||
public LauncherProcessSpec CreatePlaySpec(
|
||||
|
|
@ -99,4 +111,27 @@ public sealed class LauncherExecutableSet
|
|||
capability.Reason ?? "The selected launcher host is unavailable.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasUnixExecutePermission(string path)
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const UnixFileMode executeBits =
|
||||
UnixFileMode.UserExecute
|
||||
| UnixFileMode.GroupExecute
|
||||
| UnixFileMode.OtherExecute;
|
||||
return (File.GetUnixFileMode(path) & executeBits) != 0;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
// Fail closed if the file vanished or its metadata cannot be read
|
||||
// after the existence check. The next capability refresh retries.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ public sealed class LauncherExecutableSetTests : IDisposable
|
|||
string headless = Path.Combine(_root, "acdream-headless" + suffix);
|
||||
File.WriteAllText(graphical, string.Empty);
|
||||
File.WriteAllText(headless, string.Empty);
|
||||
MakeExecutableOnLinux(graphical);
|
||||
MakeExecutableOnLinux(headless);
|
||||
|
||||
LauncherExecutableSet set = LauncherExecutableSet.FromDirectory(_root);
|
||||
|
||||
|
|
@ -64,4 +66,78 @@ public sealed class LauncherExecutableSetTests : IDisposable
|
|||
Assert.Throws<LauncherOperationException>(() =>
|
||||
set.CreateProbeSpec("session.json"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinuxRequiresExecutePermissionForBothCoDeployedHosts()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(_root);
|
||||
string graphical = Path.Combine(_root, "AcDream.App");
|
||||
string headless = Path.Combine(_root, "acdream-headless");
|
||||
File.WriteAllText(graphical, string.Empty);
|
||||
File.WriteAllText(headless, string.Empty);
|
||||
UnixFileMode notExecutable = UnixFileMode.UserRead | UnixFileMode.UserWrite
|
||||
| UnixFileMode.GroupRead | UnixFileMode.OtherRead;
|
||||
File.SetUnixFileMode(graphical, notExecutable);
|
||||
File.SetUnixFileMode(headless, notExecutable);
|
||||
LauncherExecutableSet set = LauncherExecutableSet.FromDirectory(_root);
|
||||
|
||||
LauncherCapability gui = set.GetAvailability(LaunchMode.Gui);
|
||||
LauncherCapability headlessCapability =
|
||||
set.GetAvailability(LaunchMode.Headless);
|
||||
|
||||
Assert.False(gui.IsAvailable);
|
||||
Assert.Contains("not executable", gui.Reason, StringComparison.Ordinal);
|
||||
Assert.Contains("chmod +x", gui.Reason, StringComparison.Ordinal);
|
||||
Assert.False(headlessCapability.IsAvailable);
|
||||
Assert.Contains("not executable", headlessCapability.Reason, StringComparison.Ordinal);
|
||||
Assert.Throws<LauncherOperationException>(() =>
|
||||
set.CreatePlaySpec(LaunchMode.GuiSelect, "session.json"));
|
||||
Assert.Throws<LauncherOperationException>(() =>
|
||||
set.CreateProbeSpec("session.json"));
|
||||
|
||||
MakeExecutableOnLinux(graphical);
|
||||
MakeExecutableOnLinux(headless);
|
||||
|
||||
Assert.True(set.GetAvailability(LaunchMode.GuiSelect).IsAvailable);
|
||||
Assert.True(set.GetAvailability(LaunchMode.Headless).IsAvailable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WindowsPreservesExistenceOnlyAvailability()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var set = new LauncherExecutableSet(
|
||||
"graphical.exe",
|
||||
"headless.exe",
|
||||
fileExists: _ => true,
|
||||
hasUnixExecutePermission: _ => false);
|
||||
|
||||
Assert.True(set.GetAvailability(LaunchMode.Gui).IsAvailable);
|
||||
Assert.True(set.GetAvailability(LaunchMode.Headless).IsAvailable);
|
||||
}
|
||||
|
||||
private static void MakeExecutableOnLinux(string path)
|
||||
{
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
File.SetUnixFileMode(
|
||||
path,
|
||||
UnixFileMode.UserRead
|
||||
| UnixFileMode.UserWrite
|
||||
| UnixFileMode.UserExecute
|
||||
| UnixFileMode.GroupRead
|
||||
| UnixFileMode.GroupExecute
|
||||
| UnixFileMode.OtherRead
|
||||
| UnixFileMode.OtherExecute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -518,7 +518,8 @@ public sealed class LauncherOrchestratorTests : IDisposable
|
|||
executables ?? new LauncherExecutableSet(
|
||||
"gui-host",
|
||||
"headless-host",
|
||||
fileExists: _ => true),
|
||||
fileExists: _ => true,
|
||||
hasUnixExecutePermission: _ => true),
|
||||
new LauncherInstallRecord("dats", "pak"),
|
||||
platform ?? WindowsCapabilities(),
|
||||
configService,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,11 @@ public sealed class LauncherProjectBoundaryTests
|
|||
Assert.Contains("-getProperty:SelfContained", workflow, StringComparison.Ordinal);
|
||||
Assert.Contains("DOTNET_ROOT", workflow, StringComparison.Ordinal);
|
||||
Assert.Contains("--verify-publish", workflow, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"test -x src/AcDream.Headless/bin/Release/net10.0/acdream-headless",
|
||||
workflow,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("test -x \"$root/AcDream.App\"", workflow, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string EvaluateProperty(string projectPath, string property)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue