From 1dc5eb13b0fe25ce66928869821997d8202d240d Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sat, 25 Nov 2023 13:06:11 -0800 Subject: [PATCH 1/4] nixos/armagetronad: add module with tests --- nixos/modules/module-list.nix | 1 + nixos/modules/services/games/armagetronad.nix | 221 +++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/armagetronad.nix | 254 ++++++++++++++++++ 4 files changed, 477 insertions(+) create mode 100644 nixos/modules/services/games/armagetronad.nix create mode 100644 nixos/tests/armagetronad.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 71498e397cb6..af314370e883 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -509,6 +509,7 @@ ./services/editors/infinoted.nix ./services/finance/odoo.nix ./services/games/archisteamfarm.nix + ./services/games/armagetronad.nix ./services/games/crossfire-server.nix ./services/games/deliantra-server.nix ./services/games/factorio.nix diff --git a/nixos/modules/services/games/armagetronad.nix b/nixos/modules/services/games/armagetronad.nix new file mode 100644 index 000000000000..64b8cb23057e --- /dev/null +++ b/nixos/modules/services/games/armagetronad.nix @@ -0,0 +1,221 @@ +{ config, lib, pkgs, ... }: +let + inherit (lib) mkEnableOption mkIf mkOption mkMerge literalExpression; + inherit (lib) mapAttrsToList filterAttrs unique recursiveUpdate types; + + mkValueStringArmagetron = with lib; v: + if isInt v then toString v + else if isFloat v then toString v + else if isString v then v + else if true == v then "1" + else if false == v then "0" + else if null == v then "" + else throw "unsupported type: ${builtins.typeOf v}: ${(lib.generators.toPretty {} v)}"; + + settingsFormat = pkgs.formats.keyValue { + mkKeyValue = lib.generators.mkKeyValueDefault + { + mkValueString = mkValueStringArmagetron; + } " "; + listsAsDuplicateKeys = true; + }; + + cfg = config.services.armagetronad; + enabledServers = lib.filterAttrs (n: v: v.enable) cfg.servers; + nameToId = serverName: "armagetronad-${serverName}"; +in +{ + options = { + services.armagetronad = { + servers = mkOption { + description = lib.mdDoc "Armagetron server definitions."; + default = { }; + type = types.attrsOf (types.submodule { + options = { + enable = mkEnableOption (lib.mdDoc "armagetronad"); + package = lib.mkPackageOptionMD pkgs "armagetronad-dedicated" { + example = '' + pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated + ''; + extraDescription = '' + Ensure that you use a derivation whose evaluation contains the path `bin/armagetronad-dedicated`. + ''; + }; + host = mkOption { + type = types.str; + default = "0.0.0.0"; + description = lib.mdDoc "Host to listen on. Used for SERVER_IP."; + }; + port = mkOption { + type = types.port; + default = 4534; + description = lib.mdDoc "Port to listen on. Used for SERVER_PORT."; + }; + dns = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc "DNS address to use for this server. Optional."; + }; + openFirewall = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Set to true to open a UDP port for Armagetron Advanced."; + }; + name = mkOption { + type = types.str; + description = "The name of this server."; + }; + settings = mkOption { + type = settingsFormat.type; + default = { }; + description = lib.mdDoc '' + Armagetron Advanced server rules configuration. Refer to: + + or `armagetronad-dedicated --doc` for a list. + + This attrset is used to populate `settings_custom.cfg`; see: + + ''; + example = literalExpression '' + { + CYCLE_RUBBER = 40; + } + ''; + }; + roundSettings = mkOption { + type = settingsFormat.type; + default = { }; + description = lib.mdDoc '' + Armagetron Advanced server per-round configuration. Refer to: + + or `armagetronad-dedicated --doc` for a list. + + This attrset is used to populate `everytime.cfg`; see: + + ''; + example = literalExpression '' + { + SAY = [ + "Hosted on NixOS" + "https://nixos.org" + "iD Tech High Rubber rul3z!! Happy New Year 2008!!1" + ]; + } + ''; + }; + }; + }); + }; + }; + }; + + config = mkIf (enabledServers != { }) { + systemd.services = mkMerge (mapAttrsToList + (serverName: serverCfg: + let + serverId = nameToId serverName; + serverInfo = ( + { + SERVER_IP = serverCfg.host; + SERVER_PORT = serverCfg.port; + SERVER_NAME = serverCfg.name; + } // ( + if serverCfg.dns != null then { SERVER_DNS = serverCfg.dns; } + else { } + ) + ); + customSettings = serverCfg.settings; + everytimeSettings = serverCfg.roundSettings; + + serverInfoCfg = settingsFormat.generate "server_info.${serverName}.cfg" serverInfo; + customSettingsCfg = settingsFormat.generate "settings_custom.${serverName}.cfg" customSettings; + everytimeSettingsCfg = settingsFormat.generate "everytime.${serverName}.cfg" everytimeSettings; + in + { + "armagetronad@${serverName}" = { + description = "Armagetron Advanced Dedicated Server for ${serverName}"; + wants = [ "basic.target" ]; + after = [ "basic.target" "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = + let + stateDirectory = "armagetronad/${serverName}"; + serverRoot = "/var/lib/${stateDirectory}"; + preStart = pkgs.writeShellScript "armagetronad-${serverName}-prestart.sh" '' + owner="${serverId}:${serverId}" + + # Create the config directories. + for dirname in data settings var resource; do + dir="${serverRoot}/$dirname" + mkdir -p "$dir" + chmod u+rwx,g+rx,o-rwx "$dir" + chown "$owner" "$dir" + done + + # Link in the config files if present and non-trivial. + ln -sf ${serverInfoCfg} "${serverRoot}/settings/server_info.cfg" + ln -sf ${customSettingsCfg} "${serverRoot}/settings/settings_custom.cfg" + ln -sf ${everytimeSettingsCfg} "${serverRoot}/settings/everytime.cfg" + + # Create an input file for sending commands to the server. + input="${serverRoot}/input" + truncate -s0 "$input" + chmod u+rw,g+r,o-rwx "$input" + chown "$owner" "$input" + ''; + in + { + Type = "simple"; + StateDirectory = stateDirectory; + ExecStartPre = preStart; + ExecStart = "${serverCfg.package}/bin/armagetronad-dedicated --daemon --input ${serverRoot}/input --userdatadir ${serverRoot}/data --userconfigdir ${serverRoot}/settings --vardir ${serverRoot}/var --autoresourcedir ${serverRoot}/resource"; + Restart = "on-failure"; + CapabilityBoundingSet = ""; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + User = serverId; + Group = serverId; + }; + }; + }) + enabledServers + ); + + networking.firewall.allowedUDPPorts = + unique (mapAttrsToList (serverName: serverCfg: serverCfg.port) (filterAttrs (serverName: serverCfg: serverCfg.openFirewall) enabledServers)); + + users.users = mkMerge (mapAttrsToList + (serverName: serverCfg: + { + ${nameToId serverName} = { + group = nameToId serverName; + description = "Armagetron Advanced dedicated user for server ${serverName}"; + isSystemUser = true; + }; + }) + enabledServers + ); + + users.groups = mkMerge (mapAttrsToList + (serverName: serverCfg: + { + ${nameToId serverName} = { }; + }) + enabledServers + ); + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 81bd36cf0e34..413882bcaa7a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -128,6 +128,7 @@ in { appliance-repart-image = runTest ./appliance-repart-image.nix; apparmor = handleTest ./apparmor.nix {}; archi = handleTest ./archi.nix {}; + armagetronad = handleTest ./armagetronad.nix {}; atd = handleTest ./atd.nix {}; atop = handleTest ./atop.nix {}; atuin = handleTest ./atuin.nix {}; diff --git a/nixos/tests/armagetronad.nix b/nixos/tests/armagetronad.nix new file mode 100644 index 000000000000..d518673203a2 --- /dev/null +++ b/nixos/tests/armagetronad.nix @@ -0,0 +1,254 @@ +import ./make-test-python.nix ({ pkgs, ...} : + +let + user = "alice"; + + client = + { pkgs, ... }: + + { imports = [ ./common/user-account.nix ./common/x11.nix ]; + hardware.opengl.driSupport = true; + virtualisation.memorySize = 256; + environment = { + systemPackages = [ pkgs.armagetronad ]; + variables.XAUTHORITY = "/home/${user}/.Xauthority"; + }; + test-support.displayManager.auto.user = user; + }; + +in { + name = "armagetronad"; + meta = with pkgs.lib.maintainers; { + maintainers = [ numinit ]; + }; + + enableOCR = true; + + nodes = + { + server = { + services.armagetronad.servers = { + high-rubber = { + enable = true; + name = "Smoke Test High Rubber Server"; + port = 4534; + settings = { + SERVER_OPTIONS = "High Rubber server made to run smoke tests."; + CYCLE_RUBBER = 40; + SIZE_FACTOR = 0.5; + }; + roundSettings = { + SAY = [ + "NixOS Smoke Test Server" + "https://nixos.org" + ]; + }; + }; + sty = { + enable = true; + name = "Smoke Test sty+ct+ap Server"; + package = pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated; + port = 4535; + settings = { + SERVER_OPTIONS = "sty+ct+ap server made to run smoke tests."; + CYCLE_RUBBER = 20; + SIZE_FACTOR = 0.5; + }; + roundSettings = { + SAY = [ + "NixOS Smoke Test sty+ct+ap Server" + "https://nixos.org" + ]; + }; + }; + }; + }; + + client1 = client; + client2 = client; + }; + + testScript = let + xdo = name: text: let + xdoScript = pkgs.writeText "${name}.xdo" text; + in "${pkgs.xdotool}/bin/xdotool ${xdoScript}"; + in + '' + import shlex + import threading + from collections import namedtuple + + class Client(namedtuple('Client', ('node', 'name'))): + def send(self, *keys): + for key in keys: + self.node.send_key(key) + + def send_on(self, text, *keys): + self.node.wait_for_text(text) + self.send(*keys) + + Server = namedtuple('Server', ('node', 'name', 'address', 'port', 'welcome', 'attacker', 'victim', 'coredump_delay')) + + # Clients and their in-game names + clients = ( + Client(client1, 'Arduino'), + Client(client2, 'SmOoThIcE') + ) + + # Server configs. + servers = ( + Server(server, 'high-rubber', 'server', 4534, 'NixOS Smoke Test Server', 'SmOoThIcE', 'Arduino', 8), + Server(server, 'sty', 'server', 4535, 'NixOS Smoke Test sty+ct+ap Server', 'Arduino', 'SmOoThIcE', 8) + ) + + """ + Runs a command as the client user. + """ + def run(cmd): + return "su - ${user} -c " + shlex.quote(cmd) + + screenshot_idx = 1 + + """ + Takes screenshots on all clients. + """ + def take_screenshots(screenshot_idx): + for client in clients: + client.node.screenshot(f"screen_{client.name}_{screenshot_idx}") + return screenshot_idx + 1 + + # Wait for the servers to come up. + start_all() + for srv in servers: + srv.node.wait_for_unit(f"armagetronad@{srv.name}") + srv.node.wait_until_succeeds(f"ss --numeric --udp --listening | grep -q {srv.port}") + + # Make sure console commands work through the named pipe we created. + for srv in servers: + srv.node.succeed( + f"echo 'say Testing!' >> /var/lib/armagetronad/{srv.name}/input" + ) + srv.node.succeed( + f"echo 'say Testing again!' >> /var/lib/armagetronad/{srv.name}/input" + ) + srv.node.wait_until_succeeds( + f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: Testing!'" + ) + srv.node.wait_until_succeeds( + f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: Testing again!'" + ) + + """ + Sets up a client, waiting for the given barrier on completion. + """ + def client_setup(client, servers, barrier): + client.node.wait_for_x() + + # Configure Armagetron. + client.node.succeed( + run("mkdir -p ~/.armagetronad/var"), + run(f"echo 'PLAYER_1 {client.name}' >> ~/.armagetronad/var/autoexec.cfg") + ) + for idx, srv in enumerate(servers): + client.node.succeed( + run(f"echo 'BOOKMARK_{idx+1}_ADDRESS {srv.address}' >> ~/.armagetronad/var/autoexec.cfg"), + run(f"echo 'BOOKMARK_{idx+1}_NAME {srv.name}' >> ~/.armagetronad/var/autoexec.cfg"), + run(f"echo 'BOOKMARK_{idx+1}_PORT {srv.port}' >> ~/.armagetronad/var/autoexec.cfg") + ) + + # Start Armagetron. + client.node.succeed(run("ulimit -c unlimited; armagetronad >&2 & disown")) + client.node.wait_until_succeeds( + run( + "${xdo "create_new_win-select_main_window" '' + search --onlyvisible --name "Armagetron Advanced" + windowfocus --sync + windowactivate --sync + ''}" + ) + ) + + # Get through the tutorial. + client.send_on('Language Settings', 'ret') + client.send_on('First Setup', 'ret') + client.send_on('Welcome to Armagetron Advanced', 'ret') + client.send_on('round 1', 'esc') + client.send_on('Menu', 'up', 'up', 'ret') + client.send_on('We hope you', 'ret') + client.send_on('Armagetron Advanced', 'ret') + client.send_on('Play Game', 'ret') + + # Online > LAN > Network Setup > Mates > Server Bookmarks + client.send_on('Multiplayer', 'down', 'down', 'down', 'down', 'ret') + + barrier.wait() + + # Get to the Server Bookmarks screen on both clients. This takes a while so do it asynchronously. + barrier = threading.Barrier(3, timeout=120) + for client in clients: + threading.Thread(target=client_setup, args=(client, servers, barrier)).start() + barrier.wait() + + # Main testing loop. Iterates through each server bookmark and connects to them in sequence. + # Assumes that the game is currently on the Server Bookmarks screen. + for srv in servers: + screenshot_idx = take_screenshots(screenshot_idx) + + # Connect both clients at once, one second apart. + for client in clients: + client.send('ret') + client.node.sleep(1) + + # Wait for clients to connect + for client in clients: + srv.node.wait_until_succeeds( + f"journalctl -u armagetronad@{srv.name} -e | grep -q '{client.name}.*entered the game'" + ) + + # Wait for the match to start + srv.node.wait_until_succeeds( + f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: {srv.welcome}'" + ) + srv.node.wait_until_succeeds( + f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: https://nixos.org'" + ) + srv.node.wait_until_succeeds( + f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Go (round 1 of 10)'" + ) + + # Wait a bit + srv.node.sleep(srv.coredump_delay) + + # Turn the attacker player's lightcycle left + attacker = next(client for client in clients if client.name == srv.attacker) + victim = next(client for client in clients if client.name == srv.victim) + attacker.send('left') + screenshot_idx = take_screenshots(screenshot_idx) + + # Wait for coredump. + srv.node.wait_until_succeeds( + f"journalctl -u armagetronad@{srv.name} -e | grep -q '{attacker.name} core dumped {victim.name}'" + ) + screenshot_idx = take_screenshots(screenshot_idx) + + # Disconnect both clients from the server + for client in clients: + client.send('esc') + client.send_on('Menu', 'up', 'up', 'ret') + srv.node.wait_until_succeeds( + f"journalctl -u armagetronad@{srv.name} -e | grep -q '{client.name}.*left the game'" + ) + + # Next server. + for client in clients: + client.send_on('Server Bookmarks', 'down') + + # Stop the servers + for srv in servers: + srv.node.succeed( + f"systemctl stop armagetronad@{srv.name}" + ) + srv.node.wait_until_fails(f"ss --numeric --udp --listening | grep -q {srv.port}") + ''; + +}) From 00ab950f70707f4e1759af557d0a0bea53d10e34 Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sat, 9 Dec 2023 20:25:12 -0800 Subject: [PATCH 2/4] nixos/armagetronad: changelog --- nixos/doc/manual/release-notes/rl-2405.section.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 9dfeb6c8fe76..f7942b7e7b9d 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -63,6 +63,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m - [Clevis](https://github.com/latchset/clevis), a pluggable framework for automated decryption, used to unlock encrypted devices in initrd. Available as [boot.initrd.clevis.enable](#opt-boot.initrd.clevis.enable). +- [armagetronad](https://wiki.armagetronad.org), a mid-2000s 3D lightcycle game widely played at iD Tech Camps. You can define multiple servers using `services.armagetronad..enable`. + - [TuxClocker](https://github.com/Lurkki14/tuxclocker), a hardware control and monitoring program. Available as [programs.tuxclocker](#opt-programs.tuxclocker.enable). - [RustDesk](https://rustdesk.com), a full-featured open source remote control alternative for self-hosting and security with minimal configuration. Alternative to TeamViewer. From 133992eb0daca368c82be43ed81e65931bf6e989 Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sun, 28 Jan 2024 14:47:54 -0800 Subject: [PATCH 3/4] nixos/armagetronad: add tests for trunk --- nixos/tests/armagetronad.nix | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/nixos/tests/armagetronad.nix b/nixos/tests/armagetronad.nix index d518673203a2..be1a9bb4e92c 100644 --- a/nixos/tests/armagetronad.nix +++ b/nixos/tests/armagetronad.nix @@ -61,6 +61,23 @@ in { ]; }; }; + trunk = { + enable = true; + name = "Smoke Test trunk Server"; + package = pkgs.armagetronad."0.4".dedicated; + port = 4536; + settings = { + SERVER_OPTIONS = "0.4 server made to run smoke tests."; + CYCLE_RUBBER = 20; + SIZE_FACTOR = 0.5; + }; + roundSettings = { + SAY = [ + "NixOS Smoke Test 0.4 Server" + "https://nixos.org" + ]; + }; + }; }; }; @@ -98,7 +115,8 @@ in { # Server configs. servers = ( Server(server, 'high-rubber', 'server', 4534, 'NixOS Smoke Test Server', 'SmOoThIcE', 'Arduino', 8), - Server(server, 'sty', 'server', 4535, 'NixOS Smoke Test sty+ct+ap Server', 'Arduino', 'SmOoThIcE', 8) + Server(server, 'sty', 'server', 4535, 'NixOS Smoke Test sty+ct+ap Server', 'Arduino', 'SmOoThIcE', 8), + Server(server, 'trunk', 'server', 4536, 'NixOS Smoke Test 0.4 Server', 'Arduino', 'SmOoThIcE', 8) ) """ From a5c305d170faad291c94f3e47b1b9dc445e4dc2a Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sun, 11 Feb 2024 23:09:59 -0800 Subject: [PATCH 4/4] nixos/armagetronad: address code review feedback --- nixos/modules/services/games/armagetronad.nix | 119 ++++++++++++------ nixos/tests/armagetronad.nix | 20 +-- 2 files changed, 93 insertions(+), 46 deletions(-) diff --git a/nixos/modules/services/games/armagetronad.nix b/nixos/modules/services/games/armagetronad.nix index 64b8cb23057e..f79818e0e53b 100644 --- a/nixos/modules/services/games/armagetronad.nix +++ b/nixos/modules/services/games/armagetronad.nix @@ -23,6 +23,8 @@ let cfg = config.services.armagetronad; enabledServers = lib.filterAttrs (n: v: v.enable) cfg.servers; nameToId = serverName: "armagetronad-${serverName}"; + getStateDirectory = serverName: "armagetronad/${serverName}"; + getServerRoot = serverName: "/var/lib/${getStateDirectory serverName}"; in { options = { @@ -33,38 +35,45 @@ in type = types.attrsOf (types.submodule { options = { enable = mkEnableOption (lib.mdDoc "armagetronad"); + package = lib.mkPackageOptionMD pkgs "armagetronad-dedicated" { example = '' pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated ''; extraDescription = '' - Ensure that you use a derivation whose evaluation contains the path `bin/armagetronad-dedicated`. + Ensure that you use a derivation which contains the path `bin/armagetronad-dedicated`. ''; }; + host = mkOption { type = types.str; default = "0.0.0.0"; description = lib.mdDoc "Host to listen on. Used for SERVER_IP."; }; + port = mkOption { type = types.port; default = 4534; description = lib.mdDoc "Port to listen on. Used for SERVER_PORT."; }; + dns = mkOption { type = types.nullOr types.str; default = null; description = lib.mdDoc "DNS address to use for this server. Optional."; }; + openFirewall = mkOption { type = types.bool; default = true; - description = lib.mdDoc "Set to true to open a UDP port for Armagetron Advanced."; + description = lib.mdDoc "Set to true to open the configured UDP port for Armagetron Advanced."; }; + name = mkOption { type = types.str; description = "The name of this server."; }; + settings = mkOption { type = settingsFormat.type; default = { }; @@ -82,6 +91,7 @@ in } ''; }; + roundSettings = mkOption { type = settingsFormat.type; default = { }; @@ -110,19 +120,17 @@ in }; config = mkIf (enabledServers != { }) { - systemd.services = mkMerge (mapAttrsToList + systemd.tmpfiles.settings = mkMerge (mapAttrsToList (serverName: serverCfg: let serverId = nameToId serverName; + serverRoot = getServerRoot serverName; serverInfo = ( { SERVER_IP = serverCfg.host; SERVER_PORT = serverCfg.port; SERVER_NAME = serverCfg.name; - } // ( - if serverCfg.dns != null then { SERVER_DNS = serverCfg.dns; } - else { } - ) + } // (lib.optionalAttrs (serverCfg.dns != null) { SERVER_DNS = serverCfg.dns; }) ); customSettings = serverCfg.settings; everytimeSettings = serverCfg.roundSettings; @@ -132,43 +140,82 @@ in everytimeSettingsCfg = settingsFormat.generate "everytime.${serverName}.cfg" everytimeSettings; in { - "armagetronad@${serverName}" = { + "10-armagetronad-${serverId}" = { + "${serverRoot}/data" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/settings" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/var" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/resource" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/input" = { + "f+" = { + group = serverId; + user = serverId; + mode = "0640"; + }; + }; + "${serverRoot}/settings/server_info.cfg" = { + "L+" = { + argument = "${serverInfoCfg}"; + }; + }; + "${serverRoot}/settings/settings_custom.cfg" = { + "L+" = { + argument = "${customSettingsCfg}"; + }; + }; + "${serverRoot}/settings/everytime.cfg" = { + "L+" = { + argument = "${everytimeSettingsCfg}"; + }; + }; + }; + } + ) + enabledServers + ); + + systemd.services = mkMerge (mapAttrsToList + (serverName: serverCfg: + let + serverId = nameToId serverName; + in + { + "armagetronad-${serverName}" = { description = "Armagetron Advanced Dedicated Server for ${serverName}"; wants = [ "basic.target" ]; - after = [ "basic.target" "network.target" ]; + after = [ "basic.target" "network.target" "multi-user.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = let - stateDirectory = "armagetronad/${serverName}"; - serverRoot = "/var/lib/${stateDirectory}"; - preStart = pkgs.writeShellScript "armagetronad-${serverName}-prestart.sh" '' - owner="${serverId}:${serverId}" - - # Create the config directories. - for dirname in data settings var resource; do - dir="${serverRoot}/$dirname" - mkdir -p "$dir" - chmod u+rwx,g+rx,o-rwx "$dir" - chown "$owner" "$dir" - done - - # Link in the config files if present and non-trivial. - ln -sf ${serverInfoCfg} "${serverRoot}/settings/server_info.cfg" - ln -sf ${customSettingsCfg} "${serverRoot}/settings/settings_custom.cfg" - ln -sf ${everytimeSettingsCfg} "${serverRoot}/settings/everytime.cfg" - - # Create an input file for sending commands to the server. - input="${serverRoot}/input" - truncate -s0 "$input" - chmod u+rw,g+r,o-rwx "$input" - chown "$owner" "$input" - ''; + serverRoot = getServerRoot serverName; in { Type = "simple"; - StateDirectory = stateDirectory; - ExecStartPre = preStart; - ExecStart = "${serverCfg.package}/bin/armagetronad-dedicated --daemon --input ${serverRoot}/input --userdatadir ${serverRoot}/data --userconfigdir ${serverRoot}/settings --vardir ${serverRoot}/var --autoresourcedir ${serverRoot}/resource"; + StateDirectory = getStateDirectory serverName; + ExecStart = "${lib.getExe serverCfg.package} --daemon --input ${serverRoot}/input --userdatadir ${serverRoot}/data --userconfigdir ${serverRoot}/settings --vardir ${serverRoot}/var --autoresourcedir ${serverRoot}/resource"; Restart = "on-failure"; CapabilityBoundingSet = ""; LockPersonality = true; diff --git a/nixos/tests/armagetronad.nix b/nixos/tests/armagetronad.nix index be1a9bb4e92c..ff2841dedd21 100644 --- a/nixos/tests/armagetronad.nix +++ b/nixos/tests/armagetronad.nix @@ -138,7 +138,7 @@ in { # Wait for the servers to come up. start_all() for srv in servers: - srv.node.wait_for_unit(f"armagetronad@{srv.name}") + srv.node.wait_for_unit(f"armagetronad-{srv.name}") srv.node.wait_until_succeeds(f"ss --numeric --udp --listening | grep -q {srv.port}") # Make sure console commands work through the named pipe we created. @@ -150,10 +150,10 @@ in { f"echo 'say Testing again!' >> /var/lib/armagetronad/{srv.name}/input" ) srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: Testing!'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: Testing!'" ) srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: Testing again!'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: Testing again!'" ) """ @@ -220,18 +220,18 @@ in { # Wait for clients to connect for client in clients: srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q '{client.name}.*entered the game'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q '{client.name}.*entered the game'" ) # Wait for the match to start srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: {srv.welcome}'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: {srv.welcome}'" ) srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: https://nixos.org'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: https://nixos.org'" ) srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Go (round 1 of 10)'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Go (round 1 of 10)'" ) # Wait a bit @@ -245,7 +245,7 @@ in { # Wait for coredump. srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q '{attacker.name} core dumped {victim.name}'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q '{attacker.name} core dumped {victim.name}'" ) screenshot_idx = take_screenshots(screenshot_idx) @@ -254,7 +254,7 @@ in { client.send('esc') client.send_on('Menu', 'up', 'up', 'ret') srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q '{client.name}.*left the game'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q '{client.name}.*left the game'" ) # Next server. @@ -264,7 +264,7 @@ in { # Stop the servers for srv in servers: srv.node.succeed( - f"systemctl stop armagetronad@{srv.name}" + f"systemctl stop armagetronad-{srv.name}" ) srv.node.wait_until_fails(f"ss --numeric --udp --listening | grep -q {srv.port}") '';