smileyface/smileyface/hub_machine.py

484 lines
17 KiB
Python

import collections
import configparser
import datetime
import glob
import os
import pathlib
import subprocess
import sys
import time
import configobj
from . import myutil, structs
from ._util import md5sum_file
from .gameconfig_edit import GameIniSpecial, UnrealIniFile
class UT4ServerMachine:
def __init__(self, ctx, datalayer):
self.ctx = ctx
self.datalayer = datalayer
if not self._validate_env_vars():
sys.exit(1)
def oneclickdeploy(self):
self.generate_instance()
self.upload_redirects()
self.upload_server()
def clean_instance(self, x):
"""
Deletes the generated instance on the local machine.
"""
self.ctx.log["ut4"].info("Clearing .pak folder...")
cmd = 'rm -rv "$PROJECT_DIR"/instance/LinuxServer/UnrealTournament/Content/Paks/*'
self._invoke_command(cmd)
def create_directories(self):
"""
Create required directories which the user installs maps, mutators, and config to.
"""
dirs = ("base", "files/config", "files/maps", "files/mutators", "files/rulesets", "files/unused")
project_dir = self.ctx.config["app"]["project_dir"]
if len(project_dir.strip()) == 0:
project_dir = "."
print("project_dir:", project_dir)
fullpaths = ["/".join([project_dir, d]) for d in dirs]
for fp in fullpaths:
cmd = "mkdir -p {}".format(fp)
self._invoke_command(cmd)
def download_linux_server(self, x):
"""
Download the latest Linux Unreal Tournament 4 Server from Epic
"""
self.ctx.log["ut4"].info("Downloading Linux Server Binary from Epic.")
def download_logs(self):
"""
Download the logs from the target hub.
"""
config_dir = self.ctx.config["app"]["config_dir"]
remote_game_host = self.ctx.config["app"]["remote_game_host"]
remote_game_dir = self.ctx.config["app"]["remote_game_dir"]
self.ctx.log["ut4"].info("Downloading instance logs from target hub.")
cmd = """
rsync -ravzp {remote_game_host}:{remote_game_dir}/LinuxServer/UnrealTournament/Saved/Logs/ {config_dir}/downloaded-logs/
""".format(
**{"config_dir": config_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
)
self._invoke_command(cmd)
# Delete logs on remote game server if successfully transferred to local:
self.ctx.log["ut4"].info("")
cmd = """
ssh {remote_game_host} rm {remote_game_dir}/LinuxServer/UnrealTournament/Saved/Logs/* -r'
""".format(
**{"config_dir": config_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
)
# self._invoke_command(cmd)
def generate_instance(self):
"""
Takes the current coniguration and outputs the application files which
can be copied to the server.
"""
self.ctx.log["ut4"].info("Generating server instance from custom files...")
project_dir = self.ctx.config["app"]["project_dir"]
# rsync
src = "/".join([project_dir, "base/LinuxServer"])
dst = "/".join([project_dir, "instance/"])
cmd = "rsync -ravzp {src} {dst}".format(**{"src": src, "dst": dst})
self._invoke_command(cmd)
# cp 1
src = "/".join([project_dir, "start-server.sh"])
dst = "/".join([project_dir, "instance/"])
cmd = "cp {src} {dst}".format(**{"src": src, "dst": dst})
self._invoke_command(cmd)
# cp 2
src = "/".join([project_dir, "stop-server.sh"])
dst = "/".join([project_dir, "instance/"])
cmd = "cp {src} {dst}".format(**{"src": src, "dst": dst})
self._invoke_command(cmd)
if self._needs_first_run():
self._first_run()
self._install_config()
self._install_paks()
self._install_redirect_lines()
self._install_rulesets()
def restart_server(self):
self.stop_server()
self.start_server()
def start_server(self):
"""
Flip on the target hub on for Fragging!
"""
self.ctx.log["ut4"].info("Starting hub...")
cmd = """
ssh {remote_game_host} {remote_game_dir}/start-server.sh
"""
self._invoke_command(cmd)
def stop_server(self):
"""
Stop UT4 Hub processes on the server.
"""
self.ctx.log["ut4"].info("Stopping hub.")
cmd = """
ssh {remote_game_host} {remote_game_dir}/stop-server.sh
"""
self._invoke_command(cmd)
def upload_redirects(self):
"""
Upload paks to redirect server.
"""
self.ctx.log["ut4"].info("Uploading redirects (maps, mutators, etc.) to target hub.")
project_dir = self.ctx.config["app"]["project_dir"]
# paks_dir = os.path.join(project_dir, 'instance/LinuxServer/UnrealTournament/Content/Paks/')
paks_dir = os.path.join(project_dir, "files/") # trailing slash required
remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
cwd = project_dir
cmd = """
rsync -rivz \
--delete \
--exclude "*.md5" \
--exclude 'unused' \
--exclude ".KEEP" \
--exclude Mods.db \
{paks_dir} {remote_redirect_host}
""".format(
**{
"paks_dir": paks_dir,
"remote_redirect_host": remote_redirect_host,
}
)
# subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run
self._invoke_command(cmd)
self._redirect_hide_passwords()
self._redirect_upload_script()
self._redirect_chown()
def _redirect_hide_passwords(self):
project_dir = self.ctx.config["app"]["project_dir"]
remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
# (on the server):
gameini = "/srv/ut4-redirect.zavage.net/config/Game.ini"
engineini = "/srv/ut4-redirect.zavage.net/config/Engine.ini"
cmd = """
ssh mathewguest.com \
sed -i /ServerInstanceID=/c\ServerInstanceID=Hidden {gameini}
""".format(
gameini=gameini
)
self._invoke_command(cmd)
cmd = """
ssh mathewguest.com \
sed -i /RconPassword=/c\RconPassword=Hidden {engineini}
""".format(
engineini=engineini
)
self._invoke_command(cmd)
def _redirect_upload_script(self):
project_dir = self.ctx.config["app"]["project_dir"]
remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
cmd = """
rsync -vz \
{project_dir}/ut4-server-ctl.sh \
{remote_redirect_host}
""".format(
**{
"project_dir": project_dir,
"remote_redirect_host": remote_redirect_host,
}
)
# subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run
self._invoke_command(cmd)
def _redirect_chown(self):
project_dir = self.ctx.config["app"]["project_dir"]
remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
cmd = """
ssh mathewguest.com \
chown http:http /srv/ut4-redirect.zavage.net -R
"""
self._invoke_command(cmd)
def upload_server(self):
"""
Upload all required game files to the hub server.
"""
self.ctx.log["ut4"].info("Uploading customized server")
project_dir = self.ctx.config["app"]["project_dir"]
remote_game_host = self.ctx.config["app"]["remote_game_host"]
remote_game_dir = self.ctx.config["app"]["remote_game_dir"]
cwd = None
# transfer #1
cmd = """
rsync -raivzp \
--delete \
--exclude ".KEEP" \
--exclude "Mods.db" \
--exclude "Mod.ini" \
--exclude "Logs" \
--exclude "ut4-server.log" \
--exclude "Saved/*.ai" \
--exclude "Saved/Crashes/*" \
--exclude "Saved/Logs/*" \
{project_dir}/instance/ \
{remote_game_host}:{remote_game_dir}
""".format(
**{"project_dir": project_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
)
cmd = cmd.replace(" ", "")
# subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run
self._invoke_command(cmd)
# transfer #2
cmd = """
rsync -avzp \
{project_dir}/ut4-server-ctl.sh \
{remote_game_host}:{remote_game_dir}
""".format(
**{"project_dir": project_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
)
# subprocess.run(cmd, cwd=cwd)
self._invoke_command(cmd)
# transfer #3
cmd = """
scp \
{project_dir}/instance/ut4-server.service \
{remote_game_host}:/etc/systemd/system/
""".format(
**{"project_dir": project_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
)
# subprocess.run(cmd, cwd=cwd)
self._invoke_command(cmd)
# transfer #4
cmd = """
ssh {remote_game_host} \
chown ut4:ut4 {remote_game_dir} -R
""".format(
**{"remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
)
# subprocess.run(cmd, cwd=cwd)
self._invoke_command(cmd)
# Fix +x permissions on bash scripts
cmd = """
ssh {remote_game_host} \
chmod +x \
{remote_game_dir}/start-server.sh \
{remote_game_dir}/stop-server.sh
""".format(
**{"remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
)
# subprocess.run(cmd, cwd=cwd)
self._invoke_command(cmd)
def _first_run(self):
self.ctx.log["ut4"].info("Starting instance once to get UID.")
self.ctx.log["ut4"].info("Unfortunately, this takes 20 seconds. Just wait.")
# Make binary executable:
bin_name = "UE4Server-Linux-Shipping"
project_dir = self.ctx.config["app"]["project_dir"]
cwd = "{project_dir}/instance/LinuxServer/Engine/Binaries/Linux".format(project_dir=project_dir)
target_file = "{cwd}/{bin_name}".format(cwd=cwd, bin_name=bin_name)
cmd = ["chmod", "770", target_file]
p = subprocess.run(cmd)
cmd = ["./" + bin_name, "UnrealTournament", "UT-Entry?Game=Lobby", "-log"]
p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE)
try:
stdout, stsderr = p.communicate(timeout=20)
except subprocess.TimeoutExpired:
p.kill()
stdout, stderr = p.communicate()
self.ctx.log["ut4"].info("sleeping 20 seconds and then we'll kill the server we started just now.")
# # TODO(MG) get uid and export
def _install_config(self):
files = ("Game.ini", "Engine.ini")
project_dir = self.ctx.config["app"]["project_dir"]
config_dir = self.ctx.config["app"]["config_dir"]
for fn in files:
self.ctx.log["ut4"].info("Installing file: %s", fn)
src = os.path.join(config_dir, fn)
dst = os.path.join(project_dir, "instance/LinuxServer/UnrealTournament/Saved/Config/LinuxServer", fn)
cmd = "cp {src} {dst}".format(**{"src": src, "dst": dst})
self._invoke_command(cmd)
# Monkey-patch Game.ini to ensure it has a place for RedirectReferences
if fn == "Game.ini":
ini = UnrealIniFile(dst)
sect_name = "/Script/UnrealTournament.UTBaseGameMode"
opt_name = "RedirectReferences"
if not ini._config.has_section(sect_name):
ini._config.add_section(sect_name)
if not ini._config.has_option(sect_name, opt_name):
ini._config.set(sect_name, opt_name, ":PARAM:")
with open(dst, "w") as fp:
ini._config.write(fp)
def _install_paks(self):
project_dir = self.ctx.config["app"]["project_dir"]
self.ctx.log["ut4"].info("Installing maps...")
cmd = "rsync -ravzp {src} {dst}".format(
**{
"src": "/".join([project_dir, "files/maps/"]),
"dst": "/".join([project_dir, "instance/LinuxServer/UnrealTournament/Content/Paks/"]),
}
)
self._invoke_command(cmd)
self.ctx.log["ut4"].info("Installing mutators...")
cmd = "rsync -ravzp {src} {dst}".format(
**{
"src": "/".join([project_dir, "files/mutators/"]),
"dst": "/".join([project_dir, "instance/LinuxServer/UnrealTournament/Content/Paks/"]),
}
)
self._invoke_command(cmd)
def _install_redirect_lines(self):
self.ctx.log["ut4"].info("Generating redirect references...")
redirect_protocol = self.ctx.config["app"]["redirect_protocol"]
redirect_url = self.ctx.config["app"]["redirect_url"]
project_dir = self.ctx.config["app"]["project_dir"]
mod_dir = "/".join([project_dir, "files"])
game_ini_filepath = "/".join(
[project_dir, "instance/LinuxServer/UnrealTournament/Saved/Config/LinuxServer/Game.ini"]
)
game_ini = GameIniSpecial(game_ini_filepath)
game_ini.clear_redirect_references()
files = glob.glob("{}/**/*.pak".format(mod_dir))
redirect_lines = []
for idx, filename in enumerate(files):
# if idx > 5:
# break
md5sum = md5sum_file(filename)
p = pathlib.Path(filename)
relative_path = p.relative_to(mod_dir)
pkg_basename = p.name
# TODO(MG) Handle filenames w/o extension
try:
split = os.path.splitext(pkg_basename)
pkg_basename = split[0]
except Exception as ex:
print(ex)
continue
line = game_ini.add_redirect_reference(
**{
"pkg_basename": pkg_basename,
"redirect_protocol": redirect_protocol,
"redirect_url": redirect_url,
"relative_path": relative_path,
"md5sum": md5sum,
}
)
self.ctx.log["ut4"].debug("redirect line = '%s'", line)
data = game_ini.write(sys.stdout)
def _install_rulesets(self):
self.ctx.log["ut4"].info("Concatenating rulesets for game modes...")
project_dir = self.ctx.config["app"]["project_dir"]
src_dir = "/".join([project_dir, "files/rulesets"])
out_dir = "/".join([project_dir, "/instance/LinuxServer/UnrealTournament/Saved/Config/Rulesets"])
out_filename = "/".join([out_dir, "ruleset.json"])
cmd = "mkdir -pv {out_dir}".format(**{"out_dir": out_dir})
self._invoke_command(cmd)
self.ctx.log["ut4"].info("out filename=%s", out_filename)
# echo {\"rules\":[ > "$OUT_FILENAME"
cmd = 'echo \'{{"rules":[\' > "{out_filename}"'.format(out_filename=out_filename)
self._invoke_command(cmd)
cmd = 'for f in "{src_dir}"/*.json ; do cat "$f" >> "{out_filename}" ; done'.format(
**{"src_dir": src_dir, "out_filename": out_filename}
)
self._invoke_command(cmd)
cmd = 'echo "]}}" >> "{out_filename}"'.format(**{"out_filename": out_filename})
self._invoke_command(cmd)
self.ctx.log["ut4"].info('output ruleset is at "%s"', out_filename)
def _invoke_command(self, cmd, msg=None):
assert isinstance(cmd, str), "cmd input must be string: %s".format(cmd)
if msg is None:
msg = cmd
print(msg)
self.ctx.log["ut4"].info("running cmd: %s", cmd)
cwd = None # todo(mg) ?
os.system(cmd)
def _needs_first_run(self):
return False # TODO(MG): Hard-coded
def _validate_env_vars(self):
variable_names = (
"project_dir",
"download_url",
"download_filename",
"download_md5",
"redirect_protocol",
"redirect_url",
"remote_game_host",
"remote_game_dir",
"remote_redirect_host",
)
for name in variable_names:
value = self.ctx.config["app"][name]
self.ctx.log["ut4"].info("%s: %s", name, value)
i = input("Continue with above configuration? (y/N):")
if i.lower() != "y":
self.ctx.log["ut4"].info("Doing nothing.")
return False
self.ctx.log["ut4"].info("Continuing.")
return True
class MultiOrderedDict(collections.OrderedDict):
def __setitem__(self, key, value):
if isinstance(value, list) and key in self:
self[key].extend(value)
else:
super().__setitem__(key, value)