Skip to content

Instantly share code, notes, and snippets.

@chfanghr
Last active January 26, 2024 22:41
Show Gist options
  • Save chfanghr/b382eddb0aff578ec7bfa48cb615757c to your computer and use it in GitHub Desktop.
Save chfanghr/b382eddb0aff578ec7bfa48cb615757c to your computer and use it in GitHub Desktop.
nix-game-server-draft
# Primary logic:
#
# forever $
# let noUpd = do
# checkUpdate
# traverse activate instances
# sleep updateInterval
# retryOnError f = catch (void f) $ const $ retryOnError f
# upd = do
# traverse deactivate instances
# retryOnError update
# in catch noUpd $ const upd
#
# AttrSet Any -> String -> AttrSet arg -> Integer -> Either Integer String -> Either Integer String -> String -> String -> (String -> arg -> String) -> [String] -> AttrSet Any
lib: game: instances: updateInterval: user: group: checkUpdate: update: runServer: caps: let
managerName = "${game}-manager";
checkUpdateTimerName = "${game}-wait-check-update";
checkUpdateTaskName = "${game}-check-update";
updateTaskName = "${game}-update";
serverInstanceTemplatePrefix = "${game}-server-instance@";
mkServerInstanceName = ins: "${serverInstanceTemplatePrefix}${ins}";
serverInstanceNames = with builtins; map mkServerInstanceName (attrNames instances);
mkServiceName = s: "${s}.service";
# Top-level. So that we can kill or start every with a single command.
manager = {
description = "An unit for organizating the ${game} services.";
wantedBy = ["multi-user.target"];
after = ["network.target"];
};
# A component is a systemd unit.
# This function ensures that units are all depend on the manager.
mkComponent = c:
c
// {
unitConfig = {
PartOf = "${managerName}.target";
};
};
# Add some security related configurations.
mkServiceConfig = c:
c
// {
User = user;
Group = group;
StateDirectoryMode = "0750";
RuntimeDirectoryMode = "0750";
ProtectHome = true;
UMask = "0027";
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectProc = "invisible";
};
checkUpdateTask = let
nextServices = [(mkServiceName updateTaskName)];
in
mkComponent {
# Success when there's no need to update, crash otherwise
script = checkUpdate;
before = nextServices;
conflicts = nextServices;
serviceConfig = mkServiceConfig {
Type = "simple";
Restart = "no";
RuntimeDirectory = "${game}/update";
StateDirectory = "${game}/app";
};
# Activate all instances when no update
onSuccess = builtins.map mkServiceName serverInstanceNames;
onFailure = nextServices;
};
# The timer triggers the update task every so often.
checkUpdateTimer = mkComponent {
timerConfig = {
OnUnitActiveSec = builtins.toString updateInterval;
Unit = mkServiceName checkUpdateTaskName;
};
};
updateTask = let
nextServices = builtins.map mkServiceName serverInstanceNames;
in
mkComponent {
# Actually perform the update
script = update;
# Make sure that all instances are stopped before updating
before = nextServices;
conflicts = nextServices;
serviceConfig = mkServiceConfig {
Type = "simple";
Restart = "on-faulure";
RuntimeDirectory = "${game}/update";
# The update script should write game binary to this directory.
StateDirectory = "${game}/app";
};
unitConfig = {
StopWhenUnneeded = true;
};
# Check for update again.
onSuccess = [
(mkServiceName checkUpdateTaskName)
];
};
mkServerInstance = name: args:
let unword = with lib.lists; foldr (w: acc: "${w} ${acc}") ""; in mkComponent {
script = runServer name args;
after = updateTaskName;
serviceConfig = mkServiceConfig {
Type = "simple";
Restart = "on-failure";
RuntimeDirectory = "${game}/instances/${name}";
StateDirectory = "${game}/instances/${name}";
# Mount game binary at /opts/app
TemporaryFileSystem = "/opts:ro";
BindReadOnlyPaths = "/var/lib/${game}/app:/opts/app";
AmbientCapabilities = unword caps;
};
unitConfig = {
StopWhenUnneeded = true;
RefuseManualStart = true;
};
};
in {
systemd = {
targets.${managerName} = manager;
timers.${checkUpdateTimerName} = checkUpdateTimer;
services =
{
${checkUpdateTaskName} = checkUpdateTask;
${updateTaskName} = updateTask;
}
// (with lib.attrsets; (listToAttrs (mapAttrsToList (
name: args: let
instanceName = mkServerInstanceName name;
instanceConfig = mkServerInstance name args;
in
nameValuePair instanceName instanceConfig
)
instances)));
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment