mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-25 20:01:52 +01:00
Running now, at the top of Games, lists what's running on the Frame (Steam games, sideloaded titles, non-Steam shortcuts, Android apps), with Force quit on each, on a game's library tile and in a sideloaded title's menu. The Control Center gets a Force quit a game card. Each asks first: unsaved progress will be lost. Nothing running says so. ui/frame_running.py finds games by Steam's `reaper SteamLaunch AppId=<id>` process and its tree, never by name. It asks Steam to quit (SteamClient.Apps.TerminateApp), then sends SIGTERM and SIGKILL to the tree, skipping init, the SSH session, other users and Steam, gamescope, SteamVR, sshd and the session. Android apps also get the Apps tab's container stop. The fake Frame runs games under a stand-in reaper and lists them in SteamUIStore.RunningApps, so tests/e2e/test_running.py force quits real process trees. Not tested on a headset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
4.0 KiB
Python
83 lines
4.0 KiB
Python
"""Force quit (ui/frame_running.py) through the server's HTTP API, against games the
|
|
fake Steam client really runs under its stand-in reaper (fakesteam.REAPER)."""
|
|
import os
|
|
import unittest
|
|
|
|
import harness
|
|
from harness import api, install_title, launches, ok, ssh, state, wait_for
|
|
|
|
harness.require()
|
|
|
|
|
|
class Running(harness.FrameTestCase):
|
|
def script_title(self, name, body):
|
|
folder = self.path(name)
|
|
os.makedirs(folder)
|
|
with open(os.path.join(folder, 'run.sh'), 'w') as f:
|
|
f.write('#!/bin/sh\n' + body)
|
|
os.chmod(os.path.join(folder, 'run.sh'), 0o755)
|
|
plan, job = install_title(folder)
|
|
self.assertIsNone(job['error'], job)
|
|
return plan['id'], plan['name']
|
|
|
|
def running(self, match):
|
|
return next((g for g in ok('GET', '/api/running')['running'] if match(g)), None)
|
|
|
|
def launch_title(self, gid):
|
|
ok('POST', '/api/titles', {'action': 'launch', 'id': gid})
|
|
game = wait_for(lambda: self.running(lambda g: g.get('title') == gid), 20, f'{gid} to be running')
|
|
# The fake's reset doesn't stop programs: don't leave one running for the next test.
|
|
self.addCleanup(ssh, f"pkill -KILL -f '[d]evkit-game/{gid}/' || true", check=False)
|
|
return game
|
|
|
|
def assert_steam_alive(self, pid):
|
|
self.assertEqual(ssh(f'kill -0 {pid} && echo alive').strip(), 'alive')
|
|
|
|
def test_nothing_running(self):
|
|
self.assertEqual(ok('GET', '/api/running'), {'running': [], 'message': 'Nothing is running on the Frame'})
|
|
status, out, _ = api('POST', '/api/running', {'action': 'quit', 'appid': '620'})
|
|
self.assertEqual(status, 409, out)
|
|
|
|
def test_sigterm_quits_a_sideloaded_game(self):
|
|
gid, name = self.script_title('Hang Game', 'sleep 600 &\nwait\n')
|
|
steam = ssh('cat ~/.steam/steam.pid').strip()
|
|
game = self.launch_title(gid)
|
|
self.assertEqual((game['kind'], game['name']), ('devkit', name))
|
|
self.assertGreaterEqual(game['processes'], 3) # reaper, run.sh, sleep
|
|
out = ok('POST', '/api/running', {'action': 'quit', 'appid': str(game['appid'])})
|
|
self.assertEqual(out['method'], 'term', out)
|
|
self.assertEqual(out['message'], f'Force quit {name}')
|
|
# Steam was asked first, with the shortcut's game id.
|
|
self.assertEqual(state()['steam']['last_terminate'], game['gameid'])
|
|
self.assertIsNone(self.running(lambda g: g['appid'] == game['appid']))
|
|
self.assertEqual(ssh("pgrep -fc '[s]leep 600' || true").strip(), '0')
|
|
self.assert_steam_alive(steam)
|
|
done = wait_for(lambda: launches('devkit')[-1].get('exit') is not None and launches('devkit')[-1],
|
|
10, 'the reaper to report the exit')
|
|
self.assertIsNotNone(done['exit'])
|
|
|
|
def test_sigkill_when_it_ignores_sigterm(self):
|
|
gid, name = self.script_title('Stuck Game', "trap '' TERM\nsleep 600 &\nwait\n")
|
|
steam = ssh('cat ~/.steam/steam.pid').strip()
|
|
game = self.launch_title(gid)
|
|
out = ok('POST', '/api/running', {'action': 'quit', 'appid': str(game['appid'])})
|
|
self.assertEqual(out['method'], 'kill', out)
|
|
self.assertEqual(out['message'], f'Force quit {name} (it had to be killed)')
|
|
self.assertIsNone(self.running(lambda g: g['appid'] == game['appid']))
|
|
self.assertEqual(ssh("pgrep -fc '[s]leep 600' || true").strip(), '0')
|
|
self.assert_steam_alive(steam)
|
|
|
|
def test_steam_game_quits_through_steam(self):
|
|
ok('POST', '/api/launch', {'appid': 620980})
|
|
game = wait_for(lambda: self.running(lambda g: g['appid'] == 620980), 20, 'the Steam game to be running')
|
|
self.assertEqual((game['kind'], game['gameid']), ('steam', '620980'))
|
|
out = ok('POST', '/api/running', {'action': 'quit', 'appid': '620980'})
|
|
self.assertEqual(out['method'], 'steam', out)
|
|
self.assertEqual(state()['steam']['last_terminate'], '620980')
|
|
self.assertEqual(launches('rungameid')[-1]['exit'], 'terminated')
|
|
self.assertEqual(ok('GET', '/api/running')['running'], [])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|