diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7c75d96 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Ensure shell scripts always use LF line endings +*.sh text eol=lf diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0e7f19..d228c79 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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: @@ -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 @@ -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 diff --git a/LocalMultiplayerAgent.UnitTest/ConfigurationTests.cs b/LocalMultiplayerAgent.UnitTest/ConfigurationTests.cs index bf355e3..cc7fbd9 100644 --- a/LocalMultiplayerAgent.UnitTest/ConfigurationTests.cs +++ b/LocalMultiplayerAgent.UnitTest/ConfigurationTests.cs @@ -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; @@ -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"; @@ -348,12 +336,25 @@ public void StartGameCommandThatContainsMountPathShouldSucceed(string startGameC /// /// 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. /// [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 { @@ -406,19 +407,11 @@ public void WindowsProcessModeIsValid() /// /// 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. /// [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 { @@ -438,7 +431,7 @@ public void WindowsContainerModeIsValid() /// /// 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). /// [TestMethod] [TestCategory("BVT")] @@ -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(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 { @@ -499,7 +486,7 @@ public void WindowsGameServerEnvironmentRequiresStartGameCommand() } /// - /// Validates that assets are optional for Linux game servers. + /// Validates that assets are optional for Linux game servers in container mode. /// [TestMethod] [TestCategory("BVT")] @@ -518,11 +505,7 @@ public void LinuxGameServerAssetsAreOptional() MultiplayerSettings settings = JsonConvert.DeserializeObject(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 { @@ -557,11 +540,12 @@ public void WindowsGameServerAssetsAreRequired() } /// - /// 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. /// [TestMethod] [TestCategory("BVT")] - public void LinuxOsRejectsContainerMode() + public void LinuxOsAcceptsContainerMode() { if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux)) { @@ -579,6 +563,91 @@ public void LinuxOsRejectsContainerMode() config.AssetDetails = new JArray(); config.ContainerStartParameters.StartGameCommand = "/game/server"; + MultiplayerSettings settings = JsonConvert.DeserializeObject(config.ToString()); + settings.SetDefaultsIfNotSpecified(); + new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeTrue(); + } + finally + { + Globals.GameServerEnvironment = previousEnv; + } + } + + /// + /// On Linux OS, process mode (RunContainer=false) should also be accepted. + /// + [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(config.ToString()); + settings.SetDefaultsIfNotSpecified(); + new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeTrue(); + } + finally + { + Globals.GameServerEnvironment = previousEnv; + } + } + + /// + /// Linux process mode (RunContainer=false) requires StartGameCommand. + /// ProcessRunner crashes on null/empty StartGameCommand, so the validator must reject it. + /// + [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(config.ToString()); + settings.SetDefaultsIfNotSpecified(); + new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeFalse(); + } + finally + { + Globals.GameServerEnvironment = previousEnv; + } + } + + /// + /// Linux process mode (RunContainer=false) requires AssetDetails. + /// ProcessRunner crashes on empty AssetDetails, so the validator must reject it. + /// + [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(config.ToString()); settings.SetDefaultsIfNotSpecified(); new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeFalse(); @@ -588,6 +657,35 @@ public void LinuxOsRejectsContainerMode() Globals.GameServerEnvironment = previousEnv; } } + + /// + /// 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. + /// + [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(config.ToString()); + settings.SetDefaultsIfNotSpecified(); + new MultiplayerSettingsValidator(settings, _mockSystemOperations.Object).IsValid().Should().BeTrue(); + } + finally + { + Globals.GameServerEnvironment = previousEnv; + } + } } /// diff --git a/LocalMultiplayerAgent/Config/MultiplayerSettings.cs b/LocalMultiplayerAgent/Config/MultiplayerSettings.cs index ae20561..19d23e5 100644 --- a/LocalMultiplayerAgent/Config/MultiplayerSettings.cs +++ b/LocalMultiplayerAgent/Config/MultiplayerSettings.cs @@ -49,6 +49,13 @@ public class MultiplayerSettings public bool ForcePullFromAcrOnLinuxContainersOnWindows { get; set; } + /// + /// 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. + /// + public bool ForcePullContainerImageFromRegistry { get; set; } + public IDictionary DeploymentMetadata { get; set; } public string MaintenanceEventType { get; set; } diff --git a/LocalMultiplayerAgent/Config/MultiplayerSettingsValidator.cs b/LocalMultiplayerAgent/Config/MultiplayerSettingsValidator.cs index 4624ef9..c4ca882 100644 --- a/LocalMultiplayerAgent/Config/MultiplayerSettingsValidator.cs +++ b/LocalMultiplayerAgent/Config/MultiplayerSettingsValidator.cs @@ -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) @@ -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; } @@ -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("")) + else if (startGameCommand.Contains("PreserveNewest True - + PreserveNewest True - + + PreserveNewest + True + + + PreserveNewest + True + + PreserveNewest True @@ -45,6 +53,10 @@ Always True + + Always + True + Always True diff --git a/LocalMultiplayerAgent/MultiplayerServerManager.cs b/LocalMultiplayerAgent/MultiplayerServerManager.cs index b0130d2..612fa0d 100644 --- a/LocalMultiplayerAgent/MultiplayerServerManager.cs +++ b/LocalMultiplayerAgent/MultiplayerServerManager.cs @@ -60,14 +60,14 @@ public async Task CreateAndStartContainerWaitForExit(SessionHostsStartInfo start ISessionHostRunner sessionHostRunner = _sessionHostRunnerFactory.CreateSessionHostRunner(startParameters.SessionHostType, _vmConfiguration, _logger); - // RetrieveResources does a docker pull - // If we're running Linux containers on Windows, we might want to pull image from ACR first to save the image on local. - // In this case, you need to simply change the value to true for forcePullFromAcrOnLinuxContainersOnWindows in MultiplayerSettings.json - // so if this image is not locally built, docker create will do a docker pull first - // In another case, we might have built the image locally but tagged with a fake registry name (e.g. myacr.io/mygame:0.1), - // Then make sure to change the value to false if you want to use the image from local. - if (Globals.GameServerEnvironment == GameServerEnvironment.Windows || Globals.Settings.ForcePullFromAcrOnLinuxContainersOnWindows - || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + // RetrieveResources does a docker pull. + // Windows game servers always pull from the registry. + // For Linux containers on Windows (LCOW), set ForcePullFromAcrOnLinuxContainersOnWindows to true to pull. + // For macOS/Linux, set ForcePullContainerImageFromRegistry to true to pull from a remote registry. + // If your image is locally built (e.g. via "docker build"), leave both flags false to skip the pull. + if (Globals.GameServerEnvironment == GameServerEnvironment.Windows + || Globals.Settings.ForcePullFromAcrOnLinuxContainersOnWindows + || Globals.Settings.ForcePullContainerImageFromRegistry) { await sessionHostRunner.RetrieveResources(startParameters); } diff --git a/LocalMultiplayerAgent/Program.cs b/LocalMultiplayerAgent/Program.cs index d690596..6030d2f 100644 --- a/LocalMultiplayerAgent/Program.cs +++ b/LocalMultiplayerAgent/Program.cs @@ -34,8 +34,8 @@ public static async Task Main(string[] args) Console.WriteLine($"Check this page for debugging tips: {debuggingUrl}"); // lcow stands for Linux Containers On Windows => https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/linux-containers - // On MacOS, we only support Linux containers, so -lcow is not needed - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + // On MacOS and Linux, we only support Linux game servers, so -lcow is not needed + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { Globals.GameServerEnvironment = GameServerEnvironment.Linux; } diff --git a/LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxContainersOnLinuxSample.json b/LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxContainersOnLinuxSample.json new file mode 100644 index 0000000..e0305c6 --- /dev/null +++ b/LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxContainersOnLinuxSample.json @@ -0,0 +1,41 @@ +{ + "RunContainer": true, + "ForcePullContainerImageFromRegistry": true, + "OutputFolder": "", + "NumHeartBeatsForActivateResponse": 10, + "NumHeartBeatsForTerminateResponse": 60, + "TitleId": "", + "BuildId": "00000000-0000-0000-0000-000000000000", + "Region": "WestUs", + "AgentListeningPort": 56001, + "ContainerStartParameters": { + "ImageDetails": { + "Registry": "mydockerregistry.io", + "ImageName": "mygame", + "ImageTag": "0.1", + "Username": "", + "Password": "" + } + }, + "DeploymentMetadata": { + "Environment": "LOCAL", + "FeaturesEnabled": "List,Of,Features,Enabled" + }, + "PortMappingsList": [ + [ + { + "NodePort": 56100, + "GamePort": { + "Name": "gameport", + "Number": 7777, + "Protocol": "TCP" + } + } + ] + ], + "SessionConfig": { + "SessionId": "ba67d671-512a-4e7d-a38c-2329ce181946", + "SessionCookie": null, + "InitialPlayers": [ "Player1", "Player2" ] + } +} diff --git a/LocalMultiplayerAgent/MultiplayerSettingsLinuxContainersOnMacOSSample.json b/LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxContainersOnMacOSSample.json similarity index 95% rename from LocalMultiplayerAgent/MultiplayerSettingsLinuxContainersOnMacOSSample.json rename to LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxContainersOnMacOSSample.json index 464453b..e0305c6 100644 --- a/LocalMultiplayerAgent/MultiplayerSettingsLinuxContainersOnMacOSSample.json +++ b/LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxContainersOnMacOSSample.json @@ -1,5 +1,6 @@ { "RunContainer": true, + "ForcePullContainerImageFromRegistry": true, "OutputFolder": "", "NumHeartBeatsForActivateResponse": 10, "NumHeartBeatsForTerminateResponse": 60, diff --git a/LocalMultiplayerAgent/MultiplayerSettingsLinuxContainersOnWindowsSample.json b/LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxContainersOnWindowsSample.json similarity index 100% rename from LocalMultiplayerAgent/MultiplayerSettingsLinuxContainersOnWindowsSample.json rename to LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxContainersOnWindowsSample.json diff --git a/LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxProcessOnLinuxSample.json b/LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxProcessOnLinuxSample.json new file mode 100644 index 0000000..db5b7ab --- /dev/null +++ b/LocalMultiplayerAgent/Samples/MultiplayerSettingsLinuxProcessOnLinuxSample.json @@ -0,0 +1,40 @@ +{ + "RunContainer": false, + "OutputFolder": "", + "NumHeartBeatsForActivateResponse": 10, + "NumHeartBeatsForTerminateResponse": 60, + "TitleId": "", + "BuildId": "00000000-0000-0000-0000-000000000000", + "Region": "WestUs", + "AgentListeningPort": 56001, + "AssetDetails": [ + { + "MountPath": "", + "LocalFilePath": "" + } + ], + "DeploymentMetadata": { + "Environment": "LOCAL", + "FeaturesEnabled": "List,Of,Features,Enabled" + }, + "PortMappingsList": [ + [ + { + "NodePort": 56100, + "GamePort": { + "Name": "gameport", + "Number": 7777, + "Protocol": "TCP" + } + } + ] + ], + "ProcessStartParameters": { + "StartGameCommand": "" + }, + "SessionConfig": { + "SessionId": "ba67d671-512a-4e7d-a38c-2329ce181946", + "SessionCookie": null, + "InitialPlayers": [ "Player1", "Player2" ] + } +} diff --git a/LocalMultiplayerAgent/readme.md b/LocalMultiplayerAgent/readme.md index 443e0d6..5fe5853 100644 --- a/LocalMultiplayerAgent/readme.md +++ b/LocalMultiplayerAgent/readme.md @@ -1,6 +1,6 @@ # Development instructions for LocalMultiplayerAgent -LocalMultiplayerAgent runs on **Windows** and **MacOS (Apple Silicon) (beta)**. Please see the start up scripts for each platform for setting up firewalls and installing additional dependencies. +LocalMultiplayerAgent runs on **Windows**, **MacOS (Apple Silicon) (beta)**, and **Linux (x64) (beta)**. Please see the start up scripts for each platform for setting up firewalls and installing additional dependencies. Instructions for using LocalMultiplayerAgent can be found in the [LocalMultiplayerAgent documentation](https://learn.microsoft.com/en-us/gaming/playfab/features/multiplayer/servers/localmultiplayeragent/local-multiplayer-agent-overview). @@ -39,11 +39,11 @@ chmod +x setup_macos.sh ./setup_macos.sh ``` -Configure `MultiplayerSettings.json` with `RunContainer: true` (see `MultiplayerSettingsLinuxContainersOnMacOSSample.json` for a reference configuration). The agent automatically detects MacOS and configures for Linux containers — no additional flags are needed. +Configure `MultiplayerSettings.json` with `RunContainer: true` (see `Samples/MultiplayerSettingsLinuxContainersOnMacOSSample.json` for a reference configuration). The agent automatically detects MacOS and configures for Linux containers — no additional flags are needed. > **Troubleshooting:** If MacOS prevents you from running LocalMultiplayerAgent, run `xattr -d com.apple.quarantine ./LocalMultiplayerAgent` to remove the quarantine attribute. -For detailed instructions, see [Linux Containers on MacOS](../macos.md). +For detailed instructions, see [Linux Containers on MacOS](../docs/macos.md). ## MultiplayerSettings.json diff --git a/LocalMultiplayerAgent/setup_linux.sh b/LocalMultiplayerAgent/setup_linux.sh new file mode 100644 index 0000000..243c5f8 --- /dev/null +++ b/LocalMultiplayerAgent/setup_linux.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# This script sets up your Linux machine to run the PlayFab Multiplayer Server LocalMultiplayerAgent +# Prerequisites: Docker must be installed and running (only needed for container mode) + +set -euo pipefail + +AGENT_PORT=${1:-56001} + +echo "Setting up LocalMultiplayerAgent for Linux..." + +# Check if Docker is installed and running (only required for container mode) +if command -v docker &> /dev/null; then + if docker info &> /dev/null 2>&1; then + # Create the playfab Docker network if it doesn't exist + EXISTING_NETWORK=$(docker network ls --filter name=^playfab$ --format "{{.Name}}") + if [ -z "$EXISTING_NETWORK" ]; then + echo "Creating 'playfab' Docker network..." + docker network create playfab --driver bridge + echo "Docker network 'playfab' created successfully." + else + echo "Docker network 'playfab' already exists." + fi + else + echo "Warning: Docker is not running. Docker is required for container mode." + echo "If you plan to use process mode only, you can ignore this warning." + fi +else + echo "Warning: Docker is not installed. Docker is required for container mode." + echo "If you plan to use process mode only, you can ignore this warning." +fi + +echo "" +echo "Setup complete! You can now run LocalMultiplayerAgent." +echo "Agent will listen on port $AGENT_PORT." +echo "" +echo "Make sure to update MultiplayerSettings.json with your game server details." +echo "You can use Samples/MultiplayerSettingsLinuxContainersOnLinuxSample.json (container mode)" +echo "or Samples/MultiplayerSettingsLinuxProcessOnLinuxSample.json (process mode) as a reference." diff --git a/README.md b/README.md index 316b81c..2f861a8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This repository contains source code for the following projects: - LocalMultiplayerAgent -An executable that mimics PlayFab Multiplayer Servers (MPS) operations to aid in local debugging. It runs on **Windows** and **MacOS (Apple Silicon) (beta)**. See [PlayFab documentation - LocalMultiplayerAgent Overview](https://docs.microsoft.com/en-us/gaming/playfab/features/multiplayer/servers/localmultiplayeragent/local-multiplayer-agent-overview) to learn how to debug your game servers using LocalMultiplayerAgent. If you want to develop for Linux Containers on Windows, check out [this document](lcow.md). If you want to run on MacOS (Apple Silicon), check out [this document](macos.md). +An executable that mimics PlayFab Multiplayer Servers (MPS) operations to aid in local debugging. It runs on **Windows**, **MacOS (Apple Silicon) (beta)**, and **Linux (x64) (beta)**. See [PlayFab documentation - LocalMultiplayerAgent Overview](https://docs.microsoft.com/en-us/gaming/playfab/features/multiplayer/servers/localmultiplayeragent/local-multiplayer-agent-overview) to learn how to debug your game servers using LocalMultiplayerAgent. If you want to develop for Linux Containers on Windows, check out [this document](docs/lcow.md). If you want to run on MacOS (Apple Silicon), check out [this document](docs/macos.md). If you want to run on Linux, check out [this document](docs/linux.md). > This repository replaces and deprecates the project in [this GitHub repo](https://github.com/PlayFab/LocalMultiplayerAgent). The executable produced by that project was called `MockAgent`. This has been renamed to `LocalMultiplayerAgent` for consistency. @@ -45,6 +45,14 @@ cd ./MpsAgent/LocalMultiplayerAgent dotnet publish --runtime osx-arm64 -c Release -o LocalMultiplayerAgentPublishFolder -p:PublishSingleFile=true --self-contained true ``` +**Linux x64** (Terminal): + +```bash +git clone https://github.com/PlayFab/MpsAgent.git +cd ./MpsAgent/LocalMultiplayerAgent +dotnet publish --runtime linux-x64 -c Release -o LocalMultiplayerAgentPublishFolder -p:PublishSingleFile=true --self-contained true +``` + ## Contributing We are more than happy to accept external contributions! If you want to propose a small code change to LocalMultiplayerAgent, feel free to open a Pull Request directly. If you plan to do a bigger change to LocalMultiplayerAgent, it would be better if you open an issue describing your proposed design in order to get feedback from project maintainers. diff --git a/VmAgent.Core/Interfaces/DockerContainerEngine.cs b/VmAgent.Core/Interfaces/DockerContainerEngine.cs index 255ef07..35033b9 100644 --- a/VmAgent.Core/Interfaces/DockerContainerEngine.cs +++ b/VmAgent.Core/Interfaces/DockerContainerEngine.cs @@ -124,7 +124,7 @@ public override string GetVmAgentIpAddress() catch (Exception e) { throw new Exception( - $"Failed to find network '{SessionHostContainerConfiguration.DockerNetworkName}'. Ensure you have properly setup networking via setup.ps1", + $"Failed to find network '{SessionHostContainerConfiguration.DockerNetworkName}'. Ensure you have properly setup networking via the appropriate setup script (Setup.ps1 on Windows, setup_macos.sh on MacOS, setup_linux.sh on Linux).", e); } } diff --git a/lcow.md b/docs/lcow.md similarity index 97% rename from lcow.md rename to docs/lcow.md index 14c2dcb..cbd3fd2 100644 --- a/lcow.md +++ b/docs/lcow.md @@ -10,7 +10,7 @@ To run your containerized Linux game servers on Windows, you'll need to perform - You should mount one of your hard drives, instructions [here](https://docs.docker.com/docker-for-windows/#file-sharing) - Your game server image can be published on a container registry or can be locally built. - You should run `SetupLinuxContainersOnWindows.ps1` Powershell file which will create a Docker network called "PlayFab" -- You should properly configure your *MultiplayerSettings.json* file. Below you can see a sample, included in `MultiplayerSettingsLinuxContainersOnWindowsSample.json`: +- You should properly configure your *MultiplayerSettings.json* file. Below you can see a sample, included in `Samples/MultiplayerSettingsLinuxContainersOnWindowsSample.json`: ```json { diff --git a/docs/linux.md b/docs/linux.md new file mode 100644 index 0000000..93ab109 --- /dev/null +++ b/docs/linux.md @@ -0,0 +1,163 @@ +# Linux Support + +You can use LocalMultiplayerAgent on Linux to debug your game servers using either **container mode** (Linux containers) or **process mode** (bare processes). + +## Container Mode (Linux Containers on Linux) + +Container mode runs your game server inside a Docker container natively on Linux. + +### Prerequisites + +- [Install Docker Engine](https://docs.docker.com/engine/install/) on your Linux machine +- Make sure Docker is running (`sudo systemctl start docker`) +- Your game server image can be published on a container registry or can be locally built + +### Setup + +1. Download the latest version of LocalMultiplayerAgent for Linux (linux-x64) from the [Releases](https://github.com/PlayFab/MpsAgent/releases/) page on GitHub + +2. Run the `setup_linux.sh` script which will create a Docker network called "playfab": + +```bash +chmod +x setup_linux.sh +./setup_linux.sh +``` + +3. Configure your *MultiplayerSettings.json* file. Below you can see a sample, included in `Samples/MultiplayerSettingsLinuxContainersOnLinuxSample.json`: + +```json +{ + "RunContainer": true, + "ForcePullContainerImageFromRegistry": true, + "OutputFolder": "", + "NumHeartBeatsForActivateResponse": 10, + "NumHeartBeatsForTerminateResponse": 60, + "TitleId": "", + "BuildId": "00000000-0000-0000-0000-000000000000", + "Region": "WestUs", + "AgentListeningPort": 56001, + "ContainerStartParameters": { + "ImageDetails": { + "Registry": "mydockerregistry.io", + "ImageName": "mygame", + "ImageTag": "0.1", + "Username": "", + "Password": "" + } + }, + "PortMappingsList": [ + [ + { + "NodePort": 56100, + "GamePort": { + "Name": "gameport", + "Number": 7777, + "Protocol": "TCP" + } + } + ] + ], + "DeploymentMetadata": { + "Environment": "LOCAL", + "FeaturesEnabled": "List,Of,Features,Enabled" + }, + "SessionConfig": { + "SessionId": "ba67d671-512a-4e7d-a38c-2329ce181946", + "SessionCookie": null, + "InitialPlayers": [ "Player1", "Player2" ] + } +} +``` + +> Notes: +> 1. Set `RunContainer` to true for container mode. +> 2. Modify `ImageDetails` with your game server Docker image details. The image may be built locally (using [docker build](https://docs.docker.com/engine/reference/commandline/build/) command) or be hosted in a remote container registry. +> 3. If your image is hosted in a remote container registry, set `ForcePullContainerImageFromRegistry` to `true` so the agent pulls it before starting. If your image is locally built, set it to `false` (or omit it) to skip the pull. +> 4. `StartGameCommand` and `AssetDetails` are optional. You don't normally use them when you use a Docker container since all game assets + start game server command can be packaged in the corresponding [Dockerfile](https://docs.docker.com/engine/reference/builder/). `DeploymentMetadata` is also optional and can contain up to 30 dictionary mappings of custom metadata to be passed to the container as the `buildMetadata` field. + +4. Run LocalMultiplayerAgent: + +```bash +./LocalMultiplayerAgent +``` + +The agent will automatically detect that it is running on Linux and configure itself for Linux game servers. + +## Process Mode (Bare Processes on Linux) + +Process mode runs your game server as a native Linux process without Docker. + +### Prerequisites + +- Your game server must be a Linux executable +- Docker is **not** required for process mode + +### Setup + +1. Download the latest version of LocalMultiplayerAgent for Linux (linux-x64) from the [Releases](https://github.com/PlayFab/MpsAgent/releases/) page on GitHub + +2. Configure your *MultiplayerSettings.json* file. Below you can see a sample, included in `Samples/MultiplayerSettingsLinuxProcessOnLinuxSample.json`: + +```json +{ + "RunContainer": false, + "OutputFolder": "", + "NumHeartBeatsForActivateResponse": 10, + "NumHeartBeatsForTerminateResponse": 60, + "TitleId": "", + "BuildId": "00000000-0000-0000-0000-000000000000", + "Region": "WestUs", + "AgentListeningPort": 56001, + "AssetDetails": [ + { + "MountPath": "", + "LocalFilePath": "" + } + ], + "PortMappingsList": [ + [ + { + "NodePort": 56100, + "GamePort": { + "Name": "gameport", + "Number": 7777, + "Protocol": "TCP" + } + } + ] + ], + "ProcessStartParameters": { + "StartGameCommand": "" + }, + "SessionConfig": { + "SessionId": "ba67d671-512a-4e7d-a38c-2329ce181946", + "SessionCookie": null, + "InitialPlayers": [ "Player1", "Player2" ] + } +} +``` + +> Notes: +> 1. Set `RunContainer` to false for process mode. +> 2. `AssetDetails` is required for process mode. Set `LocalFilePath` to the path of your game server zip package. +> 3. `StartGameCommand` in `ProcessStartParameters` is required for process mode. It should be the executable name relative to the extracted assets root (e.g. `MyServer` or `MyServer --port 7777`). Arguments can be appended after a space. + +3. Run LocalMultiplayerAgent: + +```bash +./LocalMultiplayerAgent +``` + +## Building from Source + +```bash +git clone https://github.com/PlayFab/MpsAgent.git +cd ./MpsAgent/LocalMultiplayerAgent +dotnet publish --runtime linux-x64 -c Release -o LocalMultiplayerAgentPublishFolder -p:PublishSingleFile=true --self-contained true +``` + +## Troubleshooting + +- If you encounter permission issues running LocalMultiplayerAgent, make it executable: `chmod +x ./LocalMultiplayerAgent` +- For container mode, ensure your user is in the `docker` group or use `sudo`: `sudo usermod -aG docker $USER` +- If the Docker socket is not accessible, verify Docker is running: `sudo systemctl status docker` diff --git a/macos.md b/docs/macos.md similarity index 86% rename from macos.md rename to docs/macos.md index ebe5620..3a7275c 100644 --- a/macos.md +++ b/docs/macos.md @@ -15,11 +15,12 @@ chmod +x setup_macos.sh ./setup_macos.sh ``` -- Properly configure your *MultiplayerSettings.json* file. Below you can see a sample, included in `MultiplayerSettingsLinuxContainersOnMacOSSample.json`: +- Properly configure your *MultiplayerSettings.json* file. Below you can see a sample, included in `Samples/MultiplayerSettingsLinuxContainersOnMacOSSample.json`: ```json { "RunContainer": true, + "ForcePullContainerImageFromRegistry": true, "OutputFolder": "", "NumHeartBeatsForActivateResponse": 10, "NumHeartBeatsForTerminateResponse": 60, @@ -63,8 +64,9 @@ chmod +x setup_macos.sh > This file is also included in the latest LocalMultiplayerAgent zip package. Some notes: > 1. You must set `RunContainer` to true. MacOS only supports Linux containers. > 2. Modify `ImageDetails` with your game server Docker image details. The image may be built locally (using [docker build](https://docs.docker.com/engine/reference/commandline/build/) command) or be hosted in a remote container registry. -> 3. `StartGameCommand` and `AssetDetails` are optional. You don't normally use them when you use a Docker container since all game assets + start game server command can be packaged in the corresponding [Dockerfile](https://docs.docker.com/engine/reference/builder/). `DeploymentMetadata` is also optional and can contain up to 30 dictionary mappings of custom metadata to be passed to the container as the `buildMetadata` field. -> 4. Process mode (bare process without containers) is not supported on MacOS. You must use `RunContainer: true`. +> 3. If your image is hosted in a remote container registry, set `ForcePullContainerImageFromRegistry` to `true` so the agent pulls it before starting. If your image is locally built, set it to `false` (or omit it) to skip the pull. +> 4. `StartGameCommand` and `AssetDetails` are optional. You don't normally use them when you use a Docker container since all game assets + start game server command can be packaged in the corresponding [Dockerfile](https://docs.docker.com/engine/reference/builder/). `DeploymentMetadata` is also optional and can contain up to 30 dictionary mappings of custom metadata to be passed to the container as the `buildMetadata` field. +> 5. Process mode (bare process without containers) is not supported on MacOS. You must use `RunContainer: true`. - After you perform all the previous steps, you can then run LocalMultiplayerAgent: