Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ensure shell scripts always use LF line endings
*.sh text eol=lf
11 changes: 10 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
strategy:
matrix:
dotnet-version: [ '10.x' ]
os: [windows-2022, macos-14]
os: [windows-2022, macos-14, ubuntu-22.04]

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
Expand All @@ -47,6 +47,9 @@ jobs:
- name: Publish LocalMultiplayerAgent (MacOS ARM)
if: matrix.os == 'macos-14'
run: dotnet publish LocalMultiplayerAgent --runtime osx-arm64 -c Release -o LocalMultiplayerAgentPublishFolder-macos -p:PublishSingleFile=true --self-contained true
- name: Publish LocalMultiplayerAgent (Linux x64)
if: matrix.os == 'ubuntu-22.04'
run: dotnet publish LocalMultiplayerAgent --runtime linux-x64 -c Release -o LocalMultiplayerAgentPublishFolder-linux -p:PublishSingleFile=true --self-contained true
- name: Upload Windows artifact
if: matrix.os == 'windows-2022'
uses: actions/upload-artifact@v4
Expand All @@ -59,4 +62,10 @@ jobs:
with:
name: LocalMultiplayerAgent-osx-arm64
path: LocalMultiplayerAgentPublishFolder-macos
- name: Upload Linux artifact
if: matrix.os == 'ubuntu-22.04'
uses: actions/upload-artifact@v4
with:
name: LocalMultiplayerAgent-linux-x64
path: LocalMultiplayerAgentPublishFolder-linux

174 changes: 136 additions & 38 deletions LocalMultiplayerAgent.UnitTest/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,6 @@ public void SessionHostStartWithProcess()
[TestCategory("BVT")]
public void SessionHostStartWithContainer()
{
// Container mode is not supported on Linux OS, so this test only applies on Windows/MacOS
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Assert.Inconclusive("Container mode validation is not supported on Linux OS");
}

dynamic config = GetValidConfig();
config.RunContainer = true;

Expand Down Expand Up @@ -329,12 +323,6 @@ public void StartGameCommandThatDoesNotContainMountPathShouldFail(string startGa
[DataRow("C:\\Assets\\GameServer.bat")]
public void StartGameCommandThatContainsMountPathShouldSucceed(string startGameCommand)
{
// Container mode is not supported on Linux OS, so this test only applies on Windows/MacOS
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Assert.Inconclusive("Container mode validation is not supported on Linux OS");
}

dynamic config = GetValidConfig();
config.RunContainer = true;
config.AssetDetails[0].MountPath = "C:\\Assets";
Expand All @@ -348,12 +336,25 @@ public void StartGameCommandThatContainsMountPathShouldSucceed(string startGameC

/// <summary>
/// When Globals.GameServerEnvironment is Linux and RunContainer is false,
/// validation should fail because Linux game servers require container mode.
/// validation should fail on non-Linux OS because Linux game servers require container mode there.
/// On native Linux, process mode is allowed.
/// </summary>
[TestMethod]
[TestCategory("BVT")]
public void LinuxGameServerEnvironmentWithoutContainerFails()
{
// On native Linux OS, process mode is allowed for Linux game servers
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Assert.Inconclusive("On native Linux OS, Linux process mode is valid");
}

// On MacOS, process mode is rejected by the macOS-specific check above (line 118-124), which fires first
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Assert.Inconclusive("Process mode validation is not supported on MacOS");
}

var previousEnv = Globals.GameServerEnvironment;
try
{
Expand Down Expand Up @@ -406,19 +407,11 @@ public void WindowsProcessModeIsValid()
/// <summary>
/// When Globals.GameServerEnvironment is Windows and RunContainer is true (container mode),
/// validation should succeed — this is the standard Windows container scenario.
/// This test is skipped on Linux OS because the validator rejects RunContainer=true on Linux.
/// </summary>
[TestMethod]
[TestCategory("BVT")]
public void WindowsContainerModeIsValid()
{
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux))
{
// On Linux OS, RunContainer=true is rejected by the validator (container mode not yet supported on Linux)
// This test validates Windows container mode which only applies on Windows/MacOS
return;
}

var previousEnv = Globals.GameServerEnvironment;
try
{
Expand All @@ -438,7 +431,7 @@ public void WindowsContainerModeIsValid()

/// <summary>
/// Validates that StartGameCommand is not required when using Linux game server environment
/// (since it can be baked into the container image).
/// in container mode (since it can be baked into the container image via CMD/ENTRYPOINT).
/// </summary>
[TestMethod]
[TestCategory("BVT")]
Expand All @@ -452,19 +445,13 @@ public void LinuxGameServerEnvironmentAllowsEmptyStartGameCommand()
dynamic config = GetValidConfig();
config.RunContainer = true;
config.ContainerStartParameters.StartGameCommand = "";
// Remove asset details (assets are optional for Linux)
// Remove asset details (assets are optional for Linux containers)
config.AssetDetails = new JArray();

MultiplayerSettings settings = JsonConvert.DeserializeObject<MultiplayerSettings>(config.ToString());
settings.SetDefaultsIfNotSpecified();

// On non-Linux OS, RunContainer=true with Linux env should work
// On Linux OS, RunContainer=true is rejected (container mode not supported on Linux OS)
// We only test the StartGameCommand validation logic here
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux))
{
new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeTrue();
}
new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeTrue();
}
finally
{
Expand Down Expand Up @@ -499,7 +486,7 @@ public void WindowsGameServerEnvironmentRequiresStartGameCommand()
}

/// <summary>
/// Validates that assets are optional for Linux game servers.
/// Validates that assets are optional for Linux game servers in container mode.
/// </summary>
[TestMethod]
[TestCategory("BVT")]
Expand All @@ -518,11 +505,7 @@ public void LinuxGameServerAssetsAreOptional()
MultiplayerSettings settings = JsonConvert.DeserializeObject<MultiplayerSettings>(config.ToString());
settings.SetDefaultsIfNotSpecified();

// On Linux OS, RunContainer=true is rejected. On Windows/MacOS, this should be valid.
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux))
{
new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeTrue();
}
new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeTrue();
}
finally
{
Expand Down Expand Up @@ -557,11 +540,12 @@ public void WindowsGameServerAssetsAreRequired()
}

/// <summary>
/// On Linux OS, RunContainer=true should be rejected (container mode not yet supported on Linux).
/// On Linux OS, both RunContainer=true (container mode) and RunContainer=false (process mode)
/// should be accepted.
/// </summary>
[TestMethod]
[TestCategory("BVT")]
public void LinuxOsRejectsContainerMode()
public void LinuxOsAcceptsContainerMode()
{
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux))
{
Expand All @@ -579,6 +563,91 @@ public void LinuxOsRejectsContainerMode()
config.AssetDetails = new JArray();
config.ContainerStartParameters.StartGameCommand = "/game/server";

MultiplayerSettings settings = JsonConvert.DeserializeObject<MultiplayerSettings>(config.ToString());
settings.SetDefaultsIfNotSpecified();
new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeTrue();
}
finally
{
Globals.GameServerEnvironment = previousEnv;
}
}

/// <summary>
/// On Linux OS, process mode (RunContainer=false) should also be accepted.
/// </summary>
[TestMethod]
[TestCategory("BVT")]
public void LinuxOsAcceptsProcessMode()
{
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux))
{
// This test only applies when running on Linux OS
return;
}

var previousEnv = Globals.GameServerEnvironment;
try
{
Globals.GameServerEnvironment = GameServerEnvironment.Linux;

dynamic config = GetValidConfig();
config.RunContainer = false;

MultiplayerSettings settings = JsonConvert.DeserializeObject<MultiplayerSettings>(config.ToString());
settings.SetDefaultsIfNotSpecified();
new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeTrue();
}
finally
{
Globals.GameServerEnvironment = previousEnv;
}
}

/// <summary>
/// Linux process mode (RunContainer=false) requires StartGameCommand.
/// ProcessRunner crashes on null/empty StartGameCommand, so the validator must reject it.
/// </summary>
[TestMethod]
[TestCategory("BVT")]
public void LinuxProcessModeRequiresStartGameCommand()
{
var previousEnv = Globals.GameServerEnvironment;
try
{
Globals.GameServerEnvironment = GameServerEnvironment.Linux;

dynamic config = GetValidConfig();
config.RunContainer = false;
config.ProcessStartParameters.StartGameCommand = "";

MultiplayerSettings settings = JsonConvert.DeserializeObject<MultiplayerSettings>(config.ToString());
settings.SetDefaultsIfNotSpecified();
new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeFalse();
}
finally
{
Globals.GameServerEnvironment = previousEnv;
}
}

/// <summary>
/// Linux process mode (RunContainer=false) requires AssetDetails.
/// ProcessRunner crashes on empty AssetDetails, so the validator must reject it.
/// </summary>
[TestMethod]
[TestCategory("BVT")]
public void LinuxProcessModeRequiresAssets()
{
var previousEnv = Globals.GameServerEnvironment;
try
{
Globals.GameServerEnvironment = GameServerEnvironment.Linux;

dynamic config = GetValidConfig();
config.RunContainer = false;
config.AssetDetails = new JArray();

MultiplayerSettings settings = JsonConvert.DeserializeObject<MultiplayerSettings>(config.ToString());
settings.SetDefaultsIfNotSpecified();
new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeFalse();
Expand All @@ -588,6 +657,35 @@ public void LinuxOsRejectsContainerMode()
Globals.GameServerEnvironment = previousEnv;
}
}

/// <summary>
/// Linux container mode (RunContainer=true) still allows empty StartGameCommand and empty assets.
/// The Dockerfile provides CMD/ENTRYPOINT and packages all assets into the image.
/// This test ensures the process-mode fix does not regress container mode.
/// </summary>
[TestMethod]
[TestCategory("BVT")]
public void LinuxContainerModeAllowsEmptyStartGameCommandAndAssets()
{
var previousEnv = Globals.GameServerEnvironment;
try
{
Globals.GameServerEnvironment = GameServerEnvironment.Linux;

dynamic config = GetValidConfig();
config.RunContainer = true;
config.ContainerStartParameters.StartGameCommand = "";
config.AssetDetails = new JArray();

MultiplayerSettings settings = JsonConvert.DeserializeObject<MultiplayerSettings>(config.ToString());
settings.SetDefaultsIfNotSpecified();
new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeTrue();
}
finally
{
Globals.GameServerEnvironment = previousEnv;
}
}
}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions LocalMultiplayerAgent/Config/MultiplayerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ public class MultiplayerSettings

public bool ForcePullFromAcrOnLinuxContainersOnWindows { get; set; }

/// <summary>
/// When true, pulls the container image from the registry before starting.
/// Set to true when using a remote container registry on macOS or Linux.
/// When false (default), the agent assumes the image is available locally.
/// </summary>
public bool ForcePullContainerImageFromRegistry { get; set; }

public IDictionary<string, string> DeploymentMetadata { get; set; }

public string MaintenanceEventType { get; set; }
Expand Down
31 changes: 18 additions & 13 deletions LocalMultiplayerAgent/Config/MultiplayerSettingsValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ public bool IsValid(bool createBuild = false)

if (_settings.AgentListeningPort != 56001)
{
Console.WriteLine($"Warning: You have specified an AgentListeningPort ({_settings.AgentListeningPort}) that is not the default. Please make sure that port is open on your firewall by running setup.ps1 with the agent port specified.");
string setupScript = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "setup_linux.sh"
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "setup_macos.sh"
: "Setup.ps1";
Console.WriteLine($"Warning: You have specified an AgentListeningPort ({_settings.AgentListeningPort}) that is not the default. Please make sure that port is open on your firewall by running {setupScript} with the agent port specified.");
}

if (_settings.RunContainer)
Expand Down Expand Up @@ -121,15 +124,10 @@ public bool IsValid(bool createBuild = false)
}
}

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && _settings.RunContainer)
if (Globals.GameServerEnvironment == GameServerEnvironment.Linux && !_settings.RunContainer
&& !RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Console.WriteLine("Running LocalMultiplayerAgent as container mode is not yet supported on Linux. Please set RunContainer to false in MultiplayerSettings.json");
return false;
}

if (Globals.GameServerEnvironment == GameServerEnvironment.Linux && !_settings.RunContainer)
{
Console.WriteLine("The specified settings are invalid. Using Linux Game Servers requires running in a container.");
Console.WriteLine("The specified settings are invalid. Using Linux Game Servers requires running in a container (except on native Linux where process mode is also supported).");
return false;
}

Expand All @@ -154,16 +152,17 @@ public bool IsValid(bool createBuild = false)
startGameCommand = _settings.ProcessStartParameters.StartGameCommand;
}

// StartGameCommand is optional on Linux
// StartGameCommand is optional for Linux containers (the Dockerfile provides CMD/ENTRYPOINT),
// but required for process mode on any OS since ProcessRunner needs it.
if (string.IsNullOrWhiteSpace(startGameCommand))
{
if (Globals.GameServerEnvironment == GameServerEnvironment.Windows)
if (Globals.GameServerEnvironment == GameServerEnvironment.Windows || !_settings.RunContainer)
{
Console.WriteLine("StartGameCommand must be specified.");
isSuccess = false;
}
}
else if (startGameCommand.Contains("<your_game_server_exe>"))
else if (startGameCommand.Contains("<your_game_server_exe"))
{
Console.WriteLine($"StartGameCommand '{startGameCommand}' is invalid");
isSuccess = false;
Expand Down Expand Up @@ -253,9 +252,15 @@ private bool AreAssetsValid(AssetDetail[] assetDetails)
return true;
}

if (Globals.GameServerEnvironment == GameServerEnvironment.Linux && _settings.RunContainer)
{
return true; // Assets are optional for Linux containers, since we're packing the entire game onto a container image
}

if (Globals.GameServerEnvironment == GameServerEnvironment.Linux)
{
return true; // Assets are optional in Linux, since we're packing the entire game onto a container image
Console.WriteLine("Assets must be specified for game servers running in process mode.");
return false;
}

Console.WriteLine("Assets must be specified for game servers running on Windows.");
Expand Down
Loading
Loading