fix(enforcer): Steam CDP restart never worked (root+no-display, port collision)
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run

The systemd service runs as root with no User=, so _run_as_user() had no
SUDO_USER/USER to drop privileges to and launched Steam as bare root with
no DISPLAY/XAUTHORITY/DBUS session - its CDP port never opened. Add an
explicit STEAM_ENFORCER_DESKTOP_USER env var (set in the unit, substituted
by install.sh) so the enforcer always knows which desktop user owns the
Steam session, regardless of how it was launched.

Also fix a hardcoded _CDP_PORT=8080 colliding with an already-running
local service bound to 0.0.0.0:8080: requests to 127.0.0.1:8080 always
hit that service instead of steamwebhelper (which only bound
[::1]:8080), so CDP detection could never succeed even with the user
fix. Move to 9222, the conventional Chrome DevTools port.

Verified live: journalctl shows CDP ready and games hidden within ~7s
of a service restart, Steam running as the desktop user (not root).
This commit is contained in:
Krzysztof Rudnicki 2026-07-18 06:49:10 +02:00
parent d54df3a1ad
commit 00a08de076
4 changed files with 76 additions and 5 deletions

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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."""