steam-backlog-enforcer/steam_backlog_enforcer/_fast_install_cmd.py
Krzysztof kuhy Rudnicki dc22bd557c fast install: never report a failed run as success; queue the manifest first
The BG3 fast install of 2026-10-23 downloaded 160 GB, was refused by the
installer, and the web job still ended on a full bar, "Done!" and Succeeded
while Steam restarted the download from 0%.

- A failed run's reason is read from the installer's own run log (its ERROR
  lines). A waiting job prints "Failed: fast install of <game>: <reason>",
  which fails the job; `install` no longer says "Done!" after it.
- Daemon-started failures stay on the Dashboard's Fast install card (and in
  ctl status, `error`) until a new run starts or the game is installed.
- install_game writes Steam's "needs downloading" manifest before the
  installer runs: it only finishes a manifest it finds, so fresh installs
  were refused for want of one.
- Command building moved to _fast_install_cmd (250-line cap).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYmCezqhUjUmMisF5YtM4F
2026-10-24 08:36:57 +02:00

72 lines
2.4 KiB
Python

"""How steam-game-installer is invoked: where it lives, as whom, with what argv.
Split out of :mod:`_fast_install`, which runs and reaps the installer; this
half only decides the command line, every choice it would otherwise prompt for
pinned in advance.
"""
from __future__ import annotations
from pathlib import Path
import pwd
import re
from steam_backlog_enforcer._desktop_env import desktop_user_cmd
from steam_backlog_enforcer._steam_state import STEAMAPPS_PATH
_INSTALLER = Path("src", "steam-game-installer", "run.sh")
# Depot content only: the app itself is ours to choose, the language is not
# worth a prompt nobody is there to answer.
_LANGUAGE = "english"
def installer_path(user: str | None) -> Path | None:
"""steam-game-installer's ``run.sh`` in the desktop user's home, if present.
Resolved from the passwd entry rather than ``Path.home()``: the daemon runs
as root, and the installer, its venv and its credentials are per-user.
"""
try:
home = Path(pwd.getpwnam(user).pw_dir) if user else Path.home()
except KeyError:
return None
path = home / _INSTALLER
return path if path.is_file() else None
def steam_account_name() -> str | None:
"""The account Steam itself last signed in with, from ``loginusers.vdf``.
Read from Steam's own state rather than a copy in the enforcer config, so
it cannot drift from the account that actually owns the games.
"""
path = STEAMAPPS_PATH.parent / "config" / "loginusers.vdf"
try:
content = path.read_text(encoding="utf-8", errors="replace")
except OSError:
return None
match = re.search(r'"AccountName"\s+"([^"]+)"', content)
return match.group(1) if match else None
def build_cmd(installer: Path, app_id: int, user: str | None) -> list[str]:
"""The installer invocation, dropped to the desktop user when root."""
cmd = [
str(installer),
# install.sh reaches for yay; dependencies are installed once, by hand.
"--no-deps",
"--appid",
str(app_id),
"--yes",
"--language",
_LANGUAGE,
# Finishes the manifest from depotdownloader's own data, under the same
# fail-closed checks: no second login, no 70 GB re-hash by steamcmd.
"--skip-steamcmd",
]
account = steam_account_name()
if account:
cmd += ["--username", account]
return desktop_user_cmd(cmd, user)