mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 16:23:04 +02:00
fix(steam_backlog_enforcer): reload state in enforce loop, use steam:// protocol for installs
The enforce daemon loaded state once at startup and never reloaded it. When the CLI reassigned a game (e.g. via 'done'), the daemon kept enforcing the old assignment and deleted the newly assigned game every 3 seconds as 'unauthorized'. Fix: reload state from disk at the top of each enforce loop iteration so CLI changes take effect within one cycle. Also add steam://install protocol handler for interactive installs (via xdg-open) so Steam determines the correct installdir from its own metadata, avoiding 'Missing game executable' errors from guessed directory names in fabricated appmanifests.
This commit is contained in:
parent
c985160d17
commit
c4225496d3
@ -61,6 +61,26 @@ PROTECTED_APP_IDS = {
|
||||
STEAMAPPS_PATH = Path("~/.local/share/Steam/steamapps").expanduser()
|
||||
|
||||
|
||||
def _trigger_steam_install(app_id: int, label: str) -> bool:
|
||||
"""Ask Steam to install a game via the ``steam://install`` URI.
|
||||
|
||||
Returns True if the URI handler was invoked successfully.
|
||||
"""
|
||||
xdg_open = shutil.which("xdg-open") or "/usr/bin/xdg-open"
|
||||
try:
|
||||
subprocess.run(
|
||||
[xdg_open, f"steam://install/{app_id}"],
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
check=False,
|
||||
)
|
||||
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
else:
|
||||
logger.info("Triggered Steam install for %s via protocol handler", label)
|
||||
return True
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# Game install management
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
@ -142,22 +162,34 @@ def _ensure_steam_running() -> None:
|
||||
logger.exception("Steam executable not found")
|
||||
|
||||
|
||||
def install_game(app_id: int, game_name: str, steam_id: str) -> bool:
|
||||
"""Install a game by writing an appmanifest that triggers Steam's download.
|
||||
def install_game(
|
||||
app_id: int,
|
||||
game_name: str,
|
||||
steam_id: str,
|
||||
*,
|
||||
use_steam_protocol: bool = False,
|
||||
) -> bool:
|
||||
"""Install a game by triggering a Steam download.
|
||||
|
||||
Creates a minimal appmanifest with StateFlags=1026 (UpdateRequired |
|
||||
UpdateStarted) in the steamapps directory. The running Steam client
|
||||
detects the new manifest and automatically queues the download — no
|
||||
dialog or user interaction required.
|
||||
When *use_steam_protocol* is True the ``steam://install`` URI handler
|
||||
is used, which lets Steam determine the correct install directory from
|
||||
its own metadata. This avoids mismatches between the display name and
|
||||
the canonical ``installdir`` that can cause "Missing game executable"
|
||||
errors. Falls back to writing a fabricated appmanifest if the URI
|
||||
handler is unavailable.
|
||||
|
||||
If Steam is not running it will be started in silent mode first.
|
||||
When *use_steam_protocol* is False (the default) a minimal
|
||||
appmanifest with StateFlags=1026 is written directly. This is
|
||||
suitable for non-interactive / daemon contexts where opening a Steam
|
||||
dialog is undesirable.
|
||||
|
||||
Args:
|
||||
app_id: Steam application ID.
|
||||
game_name: Human-readable game name.
|
||||
steam_id: Steam64 ID of the account that owns the game.
|
||||
use_steam_protocol: Prefer the ``steam://install`` URI handler.
|
||||
|
||||
Returns True if the manifest was written successfully.
|
||||
Returns True if the install was triggered successfully.
|
||||
"""
|
||||
label = game_name or f"AppID={app_id}"
|
||||
|
||||
@ -165,6 +197,12 @@ def install_game(app_id: int, game_name: str, steam_id: str) -> bool:
|
||||
logger.info("Game already installed: %s", label)
|
||||
return True
|
||||
|
||||
if use_steam_protocol:
|
||||
_ensure_steam_running()
|
||||
if _trigger_steam_install(app_id, label):
|
||||
return True
|
||||
logger.debug("steam:// protocol failed; falling back to manifest")
|
||||
|
||||
# Build a minimal appmanifest. StateFlags 1026 = UpdateRequired (2) +
|
||||
# UpdateStarted (1024), which tells Steam "this app needs downloading".
|
||||
manifest_content = (
|
||||
|
||||
@ -236,7 +236,12 @@ def cmd_install(config: Config, state: State) -> None:
|
||||
return
|
||||
|
||||
_echo(f"Installing {state.current_game_name} (AppID={state.current_app_id})...")
|
||||
if install_game(state.current_app_id, state.current_game_name, config.steam_id):
|
||||
if install_game(
|
||||
state.current_app_id,
|
||||
state.current_game_name,
|
||||
config.steam_id,
|
||||
use_steam_protocol=True,
|
||||
):
|
||||
_echo("Done!")
|
||||
else:
|
||||
_echo("Failed to create install manifest.")
|
||||
@ -382,6 +387,7 @@ def _enforce_on_done(config: Config, state: State) -> None:
|
||||
state.current_app_id,
|
||||
state.current_game_name,
|
||||
config.steam_id,
|
||||
use_steam_protocol=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -199,7 +199,12 @@ def pick_next_game(games: list[GameInfo], state: State, config: Config) -> None:
|
||||
|
||||
if not is_game_installed(chosen.app_id):
|
||||
_echo(f"\n Auto-installing {chosen.name}...")
|
||||
install_game(chosen.app_id, chosen.name, config.steam_id)
|
||||
install_game(
|
||||
chosen.app_id,
|
||||
chosen.name,
|
||||
config.steam_id,
|
||||
use_steam_protocol=True,
|
||||
)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
@ -404,7 +409,12 @@ def _enforce_auto_install(config: Config, state: State) -> None:
|
||||
return
|
||||
if not is_game_installed(app_id):
|
||||
_echo(f" Auto-installing {state.current_game_name}...")
|
||||
if install_game(app_id, state.current_game_name, config.steam_id):
|
||||
if install_game(
|
||||
app_id,
|
||||
state.current_game_name,
|
||||
config.steam_id,
|
||||
use_steam_protocol=True,
|
||||
):
|
||||
send_notification(
|
||||
"Game Installing",
|
||||
f"{state.current_game_name} is being downloaded.",
|
||||
@ -495,6 +505,14 @@ def do_enforce(config: Config, state: State) -> None:
|
||||
_echo(" Press Ctrl+C to stop.\n")
|
||||
try:
|
||||
while True:
|
||||
# Reload state from disk so CLI changes (e.g. new game
|
||||
# assignment via ``done`` / ``scan``) take effect immediately
|
||||
# without needing to restart the daemon.
|
||||
fresh = State.load()
|
||||
state.current_app_id = fresh.current_app_id
|
||||
state.current_game_name = fresh.current_game_name
|
||||
state.finished_app_ids = fresh.finished_app_ids
|
||||
|
||||
_enforce_loop_iteration(config, state)
|
||||
time.sleep(ENFORCE_INTERVAL)
|
||||
except KeyboardInterrupt:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user