diff --git a/install.sh b/install.sh index 7ee703a..ff310d2 100755 --- a/install.sh +++ b/install.sh @@ -43,8 +43,14 @@ if [[ "${ans,,}" == "y" ]]; then SERVICE_SRC="$SCRIPT_DIR/steam-backlog-enforcer.service" SERVICE_DST="/etc/systemd/system/steam-backlog-enforcer.service" - # Set the correct working directory and PYTHONPATH in the service file. - sed "s|WorkingDirectory=.*|WorkingDirectory=$SCRIPT_DIR|; s|PYTHONPATH=.*|PYTHONPATH=$SCRIPT_DIR|" \ + # Set the correct working directory, PYTHONPATH, and desktop user (the + # user whose Steam/X11 session the service should drive) in the service + # file. install.sh requires root, so it's invoked via `sudo` and + # SUDO_USER holds the real desktop user. + DESKTOP_USER="${SUDO_USER:-$USER}" + sed "s|WorkingDirectory=.*|WorkingDirectory=$SCRIPT_DIR|; \ + s|PYTHONPATH=.*|PYTHONPATH=$SCRIPT_DIR|; \ + s|__DESKTOP_USER__|$DESKTOP_USER|" \ "$SERVICE_SRC" > "$SERVICE_DST" systemctl daemon-reload diff --git a/steam-backlog-enforcer.service b/steam-backlog-enforcer.service index 65dbe33..976781a 100644 --- a/steam-backlog-enforcer.service +++ b/steam-backlog-enforcer.service @@ -12,6 +12,8 @@ RestartSec=5 Environment=PYTHONUNBUFFERED=1 Environment=PYTHONPATH=/opt/steam-backlog-enforcer Environment=HOME=/home/kuhy +Environment=STEAM_ENFORCER_DESKTOP_USER=__DESKTOP_USER__ +Environment=DISPLAY=:0 # Hardening: enforcer must not be easily killed. OOMScoreAdjust=-900 diff --git a/steam_backlog_enforcer/library_hider.py b/steam_backlog_enforcer/library_hider.py index ec8f53d..16ea96f 100644 --- a/steam_backlog_enforcer/library_hider.py +++ b/steam_backlog_enforcer/library_hider.py @@ -31,7 +31,12 @@ import websockets logger = logging.getLogger(__name__) -_CDP_PORT = 8080 +_CDP_PORT = 9222 +# NOTE: was 8080, which collided with a different local service (Open WebUI) +# already bound to 0.0.0.0:8080. requests to 127.0.0.1:8080 resolved to that +# service instead of steamwebhelper's CDP endpoint (which only bound +# [::1]:8080), so CDP detection silently never worked. 9222 is the +# conventional Chrome DevTools debug port and was confirmed free. _CDP_TIMEOUT = 120 _STEAM_STARTUP_WAIT = 45 @@ -183,9 +188,24 @@ def _wait_for_collections_ready() -> bool: return False +def _resolve_desktop_user() -> str | None: + """Resolve which desktop user owns the Steam/X11 session. + + Prefers the explicit STEAM_ENFORCER_DESKTOP_USER (set by the systemd + unit, which has no SUDO_USER/USER of its own since it is started + directly by systemd rather than via `sudo`), then falls back to + SUDO_USER/USER for interactive `sudo` invocations. + """ + return ( + os.environ.get("STEAM_ENFORCER_DESKTOP_USER") + or os.environ.get("SUDO_USER") + or os.environ.get("USER") + ) + + def _shutdown_steam() -> None: """Send ``steam -shutdown`` and wait for the process to exit.""" - real_user = os.environ.get("SUDO_USER") or os.environ.get("USER") + real_user = _resolve_desktop_user() try: _run_as_user(["steam", "-shutdown"], real_user) except FileNotFoundError: @@ -205,7 +225,7 @@ def _shutdown_steam() -> None: def _launch_steam_with_debug() -> None: """Launch Steam with CEF debugging enabled.""" - real_user = os.environ.get("SUDO_USER") or os.environ.get("USER") + real_user = _resolve_desktop_user() _run_as_user( [ "steam", diff --git a/steam_backlog_enforcer/tests/test_library_hider_part2.py b/steam_backlog_enforcer/tests/test_library_hider_part2.py index bad1c3d..15681f0 100644 --- a/steam_backlog_enforcer/tests/test_library_hider_part2.py +++ b/steam_backlog_enforcer/tests/test_library_hider_part2.py @@ -9,6 +9,7 @@ from unittest.mock import MagicMock, patch from steam_backlog_enforcer.library_hider import ( _SPAWNED, _reap_spawned, + _resolve_desktop_user, _run_as_user, hide_other_games, restart_steam, @@ -19,6 +20,48 @@ from steam_backlog_enforcer.library_hider import ( PKG = "steam_backlog_enforcer.library_hider" +class TestResolveDesktopUser: + """Tests for _resolve_desktop_user.""" + + def test_prefers_steam_enforcer_desktop_user(self) -> None: + """The systemd unit's explicit var wins over SUDO_USER/USER.""" + with patch.dict( + os.environ, + { + "STEAM_ENFORCER_DESKTOP_USER": "kuhy", + "SUDO_USER": "someone_else", + "USER": "root", + }, + ): + assert _resolve_desktop_user() == "kuhy" + + def test_falls_back_to_sudo_user(self) -> None: + """Interactive `sudo` invocations have no explicit var set.""" + env = os.environ.copy() + env.pop("STEAM_ENFORCER_DESKTOP_USER", None) + env["SUDO_USER"] = "alice" + env["USER"] = "root" + with patch.dict(os.environ, env, clear=True): + assert _resolve_desktop_user() == "alice" + + def test_falls_back_to_user(self) -> None: + """A direct, non-sudo invocation has neither var set.""" + env = os.environ.copy() + env.pop("STEAM_ENFORCER_DESKTOP_USER", None) + env.pop("SUDO_USER", None) + env["USER"] = "kuhy" + with patch.dict(os.environ, env, clear=True): + assert _resolve_desktop_user() == "kuhy" + + def test_returns_none_when_nothing_set(self) -> None: + env = os.environ.copy() + env.pop("STEAM_ENFORCER_DESKTOP_USER", None) + env.pop("SUDO_USER", None) + env.pop("USER", None) + with patch.dict(os.environ, env, clear=True): + assert _resolve_desktop_user() is None + + class TestSteamIsInstalled: """Tests for steam_is_installed."""