Last active
January 26, 2024 22:41
-
-
Save chfanghr/b382eddb0aff578ec7bfa48cb615757c to your computer and use it in GitHub Desktop.
nix-game-server-draft
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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