mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-10 19:27:04 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
75 lines
2.8 KiB
Python
75 lines
2.8 KiB
Python
"""Spawning processes as the desktop user, and reaping them.
|
|
|
|
Split out of :mod:`steam_backlog_enforcer._steam_launch` to keep both under
|
|
the 250-line cap. Owns the fire-and-forget process registry so it has a single
|
|
owner rather than a global shared across responsibilities.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
|
|
from steam_backlog_enforcer._desktop_env import desktop_user_cmd
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Handles for fire-and-forget launches, kept only so they can be reaped.
|
|
_SPAWNED: list[subprocess.Popen[bytes]] = []
|
|
|
|
|
|
def spawn_detached(cmd: list[str]) -> subprocess.Popen[bytes]:
|
|
"""Start ``cmd`` and hand back the process, output discarded.
|
|
|
|
Fire-and-forget on purpose: the process must outlive the calling scope
|
|
(Steam itself, a desktop notifier), so it is polled or reaped later rather
|
|
than waited on in a ``with`` block here.
|
|
|
|
The unit is KillMode=control-group, so a Steam started from here used to
|
|
stay in the service's cgroup and a daemon restart SIGTERMed it together
|
|
with the running game (2026-07-26, 2026-10-02). A transient scope moves it
|
|
out; ``--scope`` execs in place, so the returned handle is still the child.
|
|
"""
|
|
if os.geteuid() == 0 and shutil.which("systemd-run"):
|
|
cmd = ["systemd-run", "--scope", "--quiet", "--collect", *cmd]
|
|
return subprocess.Popen(
|
|
cmd,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
|
|
def _reap_spawned() -> None:
|
|
"""Clear out previously launched processes that have since exited.
|
|
|
|
Launches here are fire-and-forget: Steam is meant to outlive the call, so
|
|
it is never waited on. That leaves any launch which dies immediately - a
|
|
missing binary, a broken wrapper - sitting as a zombie that still carries
|
|
the name ``steam``, which anything scanning /proc reads as "Steam is
|
|
running". Polling the old handles reaps them and retires the name.
|
|
"""
|
|
_SPAWNED[:] = [proc for proc in _SPAWNED if proc.poll() is None]
|
|
|
|
|
|
def _run_as_user(cmd: list[str], user: str | None) -> None:
|
|
"""Run a command, dropping to *user* if currently root.
|
|
|
|
Refuses to run at all when root and no desktop user resolves: the command
|
|
would otherwise execute *as root* on the user's session, which is how the
|
|
enforcer put a root Steam ("Cannot run as root user") on screen.
|
|
Failing closed costs one skipped pass; failing open costs a modal.
|
|
"""
|
|
_reap_spawned()
|
|
if os.geteuid() == 0 and (not user or user == "root"):
|
|
logger.error(
|
|
"Refusing to run %s as root: no desktop user resolved "
|
|
"(is STEAM_ENFORCER_DESKTOP_USER set?)",
|
|
cmd[0] if cmd else "<empty>",
|
|
)
|
|
return
|
|
full_cmd = desktop_user_cmd(cmd, user)
|
|
|
|
_SPAWNED.append(spawn_detached(full_cmd))
|