Files
SyncGames/agent/syncgames/watchers.py
T
DawnsorrowandCursor 0d6b0b2f80 Initial SyncGames tree: agent, Android, deploy, docs.
Session-gated MinIO save sync with AppImage GUI, CLI edit/session flow, and Gitea release helper.

Co-authored-by: Cursor <[email protected]>
2026-07-14 22:06:36 -05:00

66 lines
1.8 KiB
Python

from __future__ import annotations
import time
from typing import Callable
from syncgames.config import GameConfig
try:
import psutil
except ImportError: # pragma: no cover
psutil = None # type: ignore
def _norm(name: str) -> str:
return name.lower().removesuffix(".exe")
def is_game_running(game: GameConfig) -> bool:
if psutil is None:
return False
names = {_norm(n) for n in game.process_names}
# Steam AppID heuristic: look for steam apps running with appid in cmdline
appid = game.steam_appid
for proc in psutil.process_iter(["name", "cmdline"]):
try:
pname = _norm(proc.info["name"] or "")
if names and pname in names:
return True
if names and any(pname == _norm(n) for n in names):
return True
cmd = proc.info.get("cmdline") or []
joined = " ".join(cmd).lower()
if appid and (
f"appid={appid}" in joined
or f"steam_appid={appid}" in joined
or f"AppId={appid}" in joined
):
return True
for n in game.process_names:
if n.lower() in joined:
return True
except (psutil.Error, TypeError):
continue
return False
def watch_loop(
game: GameConfig,
on_start: Callable[[], None],
on_end: Callable[[], None],
*,
poll_s: float = 3.0,
) -> None:
"""Block forever: fire on_start when process appears, on_end when it exits."""
was_running = False
while True:
running = is_game_running(game)
if running and not was_running:
on_start()
elif not running and was_running:
# brief settle for flush
time.sleep(2.0)
on_end()
was_running = running
time.sleep(poll_s)