Skip to content

Commit

Permalink
Add feature to set systemd as subreaper
Browse files Browse the repository at this point in the history
  • Loading branch information
R1kaB3rN committed Mar 18, 2024
1 parent 9515ce4 commit 03496bb
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 12 deletions.
3 changes: 3 additions & 0 deletions ULWGL/ulwgl_consts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from enum import Enum
from pathlib import Path
from typing import Dict, Any


class Color(Enum):
Expand Down Expand Up @@ -35,3 +36,5 @@ class Color(Enum):
"getcompatpath",
"getnativepath",
}

TOMLDocument = Dict[str, Any]
56 changes: 51 additions & 5 deletions ULWGL/ulwgl_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from typing import Dict, Set, Any, List, Tuple
from argparse import Namespace
from shutil import which
from ulwgl_log import log
from ulwgl_consts import TOMLDocument, ULWGL_LOCAL


def set_env_toml(
env: Dict[str, str], args: Namespace
) -> Tuple[Dict[str, str], List[str]]:
) -> Tuple[Dict[str, str], List[str], TOMLDocument]:
"""Read a TOML file then sets the environment variables for the Steam RT.
In the TOML file, certain keys map to Steam RT environment variables. For example:
Expand Down Expand Up @@ -54,11 +56,13 @@ def set_env_toml(
opts = val
elif key == "launch_args" and isinstance(val, str):
opts = val.split(" ")
elif key == "reaper" and not val:
env["ULWGL_SYSTEMD"] = "1"

return env, opts
return env, opts, toml


def _check_env_toml(env: Dict[str, str], toml: Dict[str, Any]) -> Dict[str, Any]:
def _check_env_toml(env: Dict[str, str], toml: TOMLDocument) -> Dict[str, Any]:
"""Check for required or empty key/value pairs when reading a TOML config.
NOTE: Casing matters in the config and we do not check if the game id is set
Expand Down Expand Up @@ -92,12 +96,18 @@ def _check_env_toml(env: Dict[str, str], toml: Dict[str, Any]) -> Dict[str, Any]
err: str = f"Value for key '{key}' in TOML is not a directory: {dir}"
raise NotADirectoryError(err)

# Check for empty keys
# Check for empty and optional keys
for key, val in toml[table].items():
if key == "reaper" and not isinstance(val, bool):
err: str = (
f"Value is not a boolean for '{key}' in TOML.\n"
f"Please specify a value or remove the entry:\n{key} = {val}"
)
raise ValueError(err)
if not val and isinstance(val, str):
err: str = (
f"Value is empty for '{key}' in TOML.\n"
f"Please specify a value or remove the following entry:\n{key} = {val}"
f"Please specify a value or remove the entry:\n{key} = {val}"
)
raise ValueError(err)

Expand Down Expand Up @@ -189,3 +199,39 @@ def enable_zenity(command: str, opts: List[str], msg: str) -> None:
# Close the Zenity process's standard input
zenity_proc.stdin.close()
zenity_proc.wait()


def enable_systemd(env: Dict[str, str], command: List[str]) -> List[str]:
"""Use systemd to monitor and keep track of descendent processes.
Descendent processes of ulwgl-run will be executed in a transient, user scoped unit
For more information of systemd-run, please visit
https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html
"""
bin: str = which("systemd-run")
id: str = env["ULWGL_ID"]

if not id.startswith("ulwgl-"):
id = "ulwgl-" + env["ULWGL_ID"]

# Fallback to reaper
if not bin:
log.debug("systemd-run is not found in system\nUsing reaper as subreaper")
return enable_reaper(
env,
command,
)

# TODO Allow users to pass their own options
command.extend(
[
bin,
"--user",
"--scope",
"--send-sighup",
"--description",
id,
]
)

return command
93 changes: 86 additions & 7 deletions ULWGL/ulwgl_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,28 @@
from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
from pathlib import Path
from typing import Dict, Any, List, Set, Union, Tuple
from ulwgl_plugins import enable_steam_game_drive, set_env_toml, enable_reaper
from re import match
from subprocess import run
from ulwgl_dl_util import get_ulwgl_proton
from ulwgl_consts import PROTON_VERBS, DEBUG_FORMAT, STEAM_COMPAT, ULWGL_LOCAL
from ulwgl_util import setup_ulwgl
from ulwgl_log import log, console_handler, CustomFormatter
from ulwgl_util import UnixUser
from logging import INFO, WARNING, DEBUG
from errno import ENETUNREACH
from shutil import which
from json import loads as json_loads
from ulwgl_plugins import (
enable_steam_game_drive,
set_env_toml,
enable_reaper,
enable_systemd,
from ulwgl_consts import (
PROTON_VERBS,
DEBUG_FORMAT,
STEAM_COMPAT,
ULWGL_LOCAL,
TOMLDocument,
)


def parse_args() -> Union[Namespace, Tuple[str, List[str]]]: # noqa: D103
Expand Down Expand Up @@ -207,11 +219,23 @@ def set_env(
env["STEAM_COMPAT_TOOL_PATHS"] = env["PROTONPATH"] + ":" + ULWGL_LOCAL.as_posix()
env["STEAM_COMPAT_MOUNTS"] = env["STEAM_COMPAT_TOOL_PATHS"]

# Gamescope
if "ULWGL_GAMESCOPE" in os.environ:
env["ULWGL_GAMESCOPE"] = os.environ["ULWGL_GAMESCOPE"]

# Systemd
if "ULWGL_SYSTEMD" in os.environ:
env["ULWGL_SYSTEMD"] = os.environ["ULWGL_SYSTEMD"]

return env


def build_command(
env: Dict[str, str], local: Path, command: List[str], opts: List[str] = None
env: Dict[str, str],
local: Path,
command: List[str],
opts: List[str] = None,
config: TOMLDocument = None,
) -> List[str]:
"""Build the command to be executed."""
verb: str = env["PROTON_VERB"]
Expand All @@ -231,7 +255,20 @@ def build_command(
err: str = "The following file was not found in PROTONPATH: proton"
raise FileNotFoundError(err)

enable_reaper(env, command, local)
# Subreaper
if (
config
and config.get("ulwgl").get("reaper")
and not config.get("ulwgl").get("reaper")
):
log.debug("Using systemd as subreaper")
enable_systemd(env, command)
elif env.get("ULWGL_SYSTEMD") == "1":
log.debug("Using systemd as subreaper")
enable_systemd(env, command)
else:
log.debug("Using reaper as subreaper")
enable_reaper(env, command, local)

command.extend([local.joinpath("ULWGL").as_posix(), "--verb", verb, "--"])
command.extend(
Expand Down Expand Up @@ -270,6 +307,7 @@ def main() -> int: # noqa: D103
"STORE": "",
"PROTON_VERB": "",
"ULWGL_ID": "",
"ULWGL_SYSTEMD": "",
}
command: List[str] = []
opts: List[str] = None
Expand All @@ -279,6 +317,7 @@ def main() -> int: # noqa: D103
# Expects this dir to be in sync with root
# On update, files will be selectively updated
args: Union[Namespace, Tuple[str, List[str]]] = parse_args()
config: TOMLDocument = None

if "musl" in os.environ.get("LD_LIBRARY_PATH", ""):
err: str = "This script is not designed to run on musl-based systems"
Expand Down Expand Up @@ -316,7 +355,7 @@ def main() -> int: # noqa: D103

# Check environment
if isinstance(args, Namespace) and getattr(args, "config", None):
env, opts = set_env_toml(env, args)
env, opts, config = set_env_toml(env, args)
else:
opts = args[1] # Reference the executable options
check_env(env)
Expand All @@ -337,17 +376,57 @@ def main() -> int: # noqa: D103
os.environ[key] = val

# Run
build_command(env, ULWGL_LOCAL, command, opts)
build_command(env, ULWGL_LOCAL, command, opts, config)
log.debug(command)

return run(command).returncode


def stop_unit(id: str) -> None:
"""Handle explicit kill or server shutdowns of the game.
Intended to run when using gamescope with the launcher
Used when systemd is configured as the subreaper and we assume the systemd
transient unit is ours if the unit's description matches the ULWGL ID
"""
if not os.environ["ULWGL_SYSTEMD"] == "1" and os.environ["ULWGL_GAMESCOPE"] == "1":
emoji: str = "\U0001f480"
log.warning("Explicit shutdown detected")
log.warning("Zombies will prevent re-running the game %s ...", emoji)
return

result: str = run(
[which("systemctl"), "list-units", "--user", "-o", "json"],
capture_output=True,
text=True,
).stdout
ulwgl_id: str = f"ulwgl-{id}"
unit: str = ""

for item in json_loads(result):
if item.get("description") == ulwgl_id:
unit = item["unit"]
break

if unit:
emoji: str = "\U0001f480"
log.console(f"Reaping zombies due to explicit shutdown {emoji} ...")
run([which("systemctl"), "stop", "--user", f"{unit}"])


if __name__ == "__main__":
try:
sys.exit(main())
ret: int = main()

if not ret:
# Handle force exits when using gamescope
stop_unit(os.environ["ULWGL_ID"])

sys.exit(ret)
except KeyboardInterrupt:
log.warning("Keyboard Interrupt")
stop_unit(os.environ["ULWGL_ID"])
sys.exit(1)
except SystemExit as e:
if e.code != 0:
Expand Down
Loading

0 comments on commit 03496bb

Please sign in to comment.