fix(session): wait for Steam readiness before launch (#2362)

* fix(session): wait for Steam readiness before launch

* fix(session): keep the launch fallback when Steam readiness or the interstitial is unavailable

* fix(session): cap the Steam readiness wait by elapsed time and bound each check

* fix(session): keep waiting for Steam readiness through focus changes and never pass a zero timeout
This commit is contained in:
MaxsTechReview authored and GitHub committed 2026-11-09 18:23:24 -05:00
1 parent 836340f3a2
commit 6bcd431ca6
4 files changed
+97 -14

No files matched your search

@@ -927,6 +927,33 @@ def steam_shortcut_state(agent_dir, game_id, timeout=6):
time.sleep(min(0.5, max(0, deadline - time.monotonic())))
def steam_ready(agent_dir):
expression = """(() => {
const store = globalThis.appStore;
return !!store?.m_bIsInitialized && typeof globalThis.SteamClient?.Apps?.ContinueGameAction === 'function';
})()"""
return cdp_eval(agent_dir, expression, await_promise=False, timeout=3).get("value") is True
def steam_clear_pending_interstitial(agent_dir, app_id):
if not isinstance(app_id, str) or not app_id.isascii() or not app_id.isdigit() or int(app_id) <= 0:
raise AgentError("INVALID_APP_ID", "appId must be a positive decimal Steam app id")
expression = """(async () => {
const apps = globalThis.SteamClient?.Apps;
if (typeof apps?.GetActiveGameActions !== 'function' || typeof apps.ContinueGameAction !== 'function') return false;
const active = await apps.GetActiveGameActions() || [];
for (const action of active) {
const appid = String(action?.strAppID ?? action?.gameid ?? action?.nAppID ?? '');
if (appid === %s && action?.strTaskName === 'ShowInterstitials') {
apps.ContinueGameAction(action.nGameActionID, appid, 'ShowInterstitials');
return true;
}
}
return false;
})()""" % json.dumps(app_id)
return cdp_eval(agent_dir, expression, timeout=3).get("value") is True
def game_pids(app_id):
wanted = [("SteamAppId=%s" % app_id).encode(), ("SteamGameId=%s" % app_id).encode()]
me = os.getpid()
@@ -1261,6 +1288,19 @@ def main(argv):
return 1
print(state)
return {"ready": 0, "missing": 4, "not-shortcut": 3}.get(state, 1)
if argv and argv[0] == "--steam-ready":
try:
return 0 if steam_ready(Path(base) / "agent") else 1
except (AgentError, OSError, ValueError):
return 1
if argv and argv[0] == "--steam-clear-pending-interstitial":
if len(argv) != 2:
print("usage: droiddeck-agent --steam-clear-pending-interstitial <appid>", file=sys.stderr)
return 2
try:
return 0 if steam_clear_pending_interstitial(Path(base) / "agent", argv[1]) else 1
except (AgentError, OSError, ValueError):
return 1
serve(Path(base) / "agent", focus="--no-focus" not in argv)
return 0
@@ -1244,7 +1244,14 @@ case $mode in
sleep 1
done
[ "${#game}" -gt 10 ] && shortcut=1
[ -n "$shortcut" ] || sleep 4
_bl_ready_end=$((SECONDS + 30))
while [ "$waited" -lt 30 ] && [ "$((_bl_ready_end - SECONDS))" -gt 0 ]; do
timeout "$((_bl_ready_end - SECONDS))" /usr/local/bin/droiddeck-agent --steam-ready >/dev/null 2>&1 && break
[ "$((_bl_ready_end - SECONDS))" -gt 1 ] || break
sleep 1
waited=$((waited + 1))
done
waited=0
echo "== steam: interface up; launching the requested game $game"
_bl_hand_off
[ -n "$shortcut" ] || return 0
@@ -1264,7 +1271,9 @@ case $mode in
elif _bl_starting; then
echo "== steam: the requested game started while waiting; not launching again"
elif [ "$(_bl_focused)" = 769 ]; then
echo "== steam: launching the requested game again $game (overview status $status)"
echo "== steam: clearing the pending launch interstitial for $game (overview status $status)"
/usr/local/bin/droiddeck-agent --steam-clear-pending-interstitial "$(((game >> 32) & 0xffffffff))" >/dev/null 2>&1 && return 0
echo "== steam: launching the requested game again $game"
_bl_hand_off
else
echo "== steam: the game took focus while waiting; not launching again"
+20
View File
@@ -457,6 +457,26 @@ class AgentTest(unittest.TestCase):
self.assertEqual(1, agent.main(["--steam-shortcut-state", game_id]))
self.assertEqual(2, agent.main(["--steam-shortcut-state"]))
def test_steam_ready_uses_steams_initialized_store(self):
with patch.object(agent, "cdp_eval", return_value={"value": True}) as evaluate:
self.assertTrue(agent.steam_ready(self.dir))
self.assertIn("m_bIsInitialized", evaluate.call_args.args[1])
with patch.object(agent, "cdp_eval", return_value={"value": False}):
self.assertFalse(agent.steam_ready(self.dir))
def test_pending_interstitial_is_continued_by_steam(self):
with patch.object(agent, "cdp_eval", return_value={"value": True}) as evaluate:
self.assertTrue(agent.steam_clear_pending_interstitial(self.dir, "3551182975"))
expression = evaluate.call_args.args[1]
self.assertIn("ContinueGameAction", expression)
self.assertIn("ShowInterstitials", expression)
self.assertIn('"3551182975"', expression)
self.assertIn("await apps.GetActiveGameActions()", expression)
with patch.object(agent, "cdp_eval", return_value={"value": False}):
self.assertFalse(agent.steam_clear_pending_interstitial(self.dir, "3551182975"))
with self.assertRaises(agent.AgentError):
agent.steam_clear_pending_interstitial(self.dir, "0")
def test_asleep_command_reports_steams_own_sleep_state(self):
with patch.dict(os.environ, {"BL_LAUNCH_DIR": str(self.dir.parent)}):
for value, code in (("yes", 0), ("no", 5), ("unknown", 1), (None, 1)):
+26 -12
View File
@@ -41,6 +41,7 @@ class SteamColdLaunchTest(unittest.TestCase):
"#!/bin/sh\n"
"printf '%s\n' \"$@\" > \"$AGENT_ARGS\"\n"
"n=$(cat \"$AGENT_COUNT\" 2>/dev/null || echo 0); n=$((n + 1)); echo $n > \"$AGENT_COUNT\"\n"
"[ \"$1\" = --steam-ready ] && exit \"${READY_STATUS:-0}\"\n"
"exit \"$AGENT_STATUS\"\n")
agent.chmod(0o755)
sleep = self.bin / "sleep"
@@ -55,12 +56,13 @@ class SteamColdLaunchTest(unittest.TestCase):
pgrep.chmod(0o755)
self.block = LAUNCH_BLOCK.replace("/usr/local/bin/droiddeck-agent", str(agent))
def run_launch(self, target, focus="769", helper_status=0, running_after=999):
def run_launch(self, target, focus="769", helper_status=0, running_after=999, ready_status=0):
env = dict(os.environ, HOME=str(self.root), BL_LAUNCH_DIR=str(self.launch_dir),
PATH=str(self.bin) + os.pathsep + os.environ["PATH"],
AGENT_ARGS=str(self.agent_args), SLEEP_ARGS=str(self.sleep_args),
AGENT_COUNT=str(self.agent_count), AGENT_STATUS=str(helper_status),
FOCUS=focus, FOCUS_COUNT=str(self.focus_count), RUNNING_AFTER=str(running_after))
FOCUS=focus, FOCUS_COUNT=str(self.focus_count), RUNNING_AFTER=str(running_after),
READY_STATUS=str(ready_status))
script = self.block + "\nprintf 'ARG:%s\n' \"$@\"\nwait\n"
url = target if target.startswith("steam://") else "steam://rungameid/" + target
result = subprocess.run(["bash", "-c", script, "cold-launch", url],
@@ -72,28 +74,40 @@ class SteamColdLaunchTest(unittest.TestCase):
return [l for l in result.stdout.splitlines() if "launching the requested game" in l]
def test_a_shortcut_that_takes_focus_is_handed_over_once_without_waiting_for_the_overview(self):
result = self.run_launch(SHORTCUT, focus="769 769 4242")
result = self.run_launch(SHORTCUT, focus="769 4242")
self.assertEqual(SHORTCUT, (self.launch_dir / "steam-game").read_text().strip())
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
self.assertFalse(self.agent_args.exists(), result.stdout)
self.assertEqual(["1"], self.sleep_args.read_text().split())
self.assertEqual(["--steam-ready"], self.agent_args.read_text().splitlines())
self.assertFalse(self.sleep_args.exists())
def test_a_shortcut_that_never_takes_focus_is_handed_over_again_after_the_overview_wait(self):
result = self.run_launch(SHORTCUT, focus="769", helper_status=0)
self.assertEqual(2, len(self.hand_offs(result)), result.stdout)
self.assertIn("launching the requested game again", result.stdout)
self.assertEqual(["--wait-steam-shortcut-overview", SHORTCUT, "45"], self.agent_args.read_text().splitlines())
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
self.assertIn("clearing the pending launch interstitial", result.stdout)
self.assertEqual(["--steam-clear-pending-interstitial", "3551182975"], self.agent_args.read_text().splitlines())
self.assertEqual(["1"] * 20, self.sleep_args.read_text().split())
def test_a_timed_out_overview_still_gets_one_more_hand_off(self):
result = self.run_launch(SHORTCUT, focus="769", helper_status=1)
self.assertEqual(2, len(self.hand_offs(result)), result.stdout)
self.assertIn("overview status 1", result.stdout)
self.assertIn("launching the requested game again", result.stdout)
def test_a_steam_that_never_reports_ready_still_gets_the_launch(self):
result = self.run_launch("413150", ready_status=1)
self.assertEqual("413150", (self.launch_dir / "steam-game").read_text().strip())
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
self.assertEqual(["1"] * 30, self.sleep_args.read_text().split())
def test_a_focus_change_does_not_end_the_readiness_wait(self):
result = self.run_launch("413150", focus="769 4242", ready_status=1)
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
self.assertEqual(["1"] * 30, self.sleep_args.read_text().split())
def test_a_game_still_starting_without_a_window_is_not_launched_twice(self):
result = self.run_launch(SHORTCUT, running_after=1)
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
self.assertFalse(self.agent_args.exists())
self.assertIn("--steam-ready", self.agent_args.read_text())
self.assertEqual(["-f", "reaper SteamLaunch AppId=3551182975( |$)"],
(self.root / "pgrep-args").read_text().splitlines())
self.assertIn("already starting", result.stdout)
@@ -108,15 +122,15 @@ class SteamColdLaunchTest(unittest.TestCase):
focus = " ".join(["769"] * 23) + " 4242"
result = self.run_launch(SHORTCUT, focus=focus, helper_status=0)
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
self.assertIn("took focus while waiting", result.stdout)
self.assertNotIn("clearing the pending launch interstitial", result.stdout)
self.assertNotIn("launching the requested game again", result.stdout)
def test_native_app_id_keeps_the_short_delay_and_is_handed_over_once(self):
result = self.run_launch("413150")
self.assertEqual("413150", (self.launch_dir / "steam-game").read_text().strip())
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
self.assertEqual("4", self.sleep_args.read_text().strip())
self.assertFalse(self.agent_args.exists())
self.assertFalse(self.sleep_args.exists())
self.assertIn("--steam-ready", self.agent_args.read_text())
def test_a_restart_for_a_game_uses_the_same_hand_off(self):
loop = SOURCE[SOURCE.index(' if [ -s "$HOME/.bl-restart-game" ]; then'):SOURCE.index(' if [ -f "$HOME/.bl-restart-once" ]; then')]