mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 06:00:33 +02:00
Android launcher: stop an orphaned container instead of refusing Play; keep the lock out of Lepton's tree
Once flock is held no launcher owns a running container (a SIGKILLed launcher left it), so it is stopped and the launch continues. fd 9 is closed for the Lepton child so it can't keep the lock held. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
060801c674
commit
70897cfd1d
2 files changed
+17
-7
No files matched your search
@@ -30,9 +30,11 @@ fi
|
||||
exec 9>"$DIR/launch.lock"
|
||||
flock -n 9 || { echo "Android app is already running" >&2; exit 1; }
|
||||
CONTAINER="lepton-steamlaunch-$SteamAppId"
|
||||
# Holding the lock means no launcher owns a running container: it was orphaned
|
||||
# (this script SIGKILLed), so stop it rather than refuse every later Play.
|
||||
if [[ "$(podman inspect --format '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true)" == true ]]; then
|
||||
echo "Android container is already running" >&2
|
||||
exit 1
|
||||
echo "Stopping orphaned $CONTAINER" >&2
|
||||
podman stop -t 5 "$CONTAINER" >/dev/null 2>&1 || true
|
||||
fi
|
||||
export STEAM_COMPAT_INSTALL_PATH="$DIR"
|
||||
# Must be under ~/.local/share/Steam: only that tree is mounted in the container.
|
||||
@@ -63,7 +65,8 @@ trap cleanup EXIT
|
||||
trap 'exit 143' TERM
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 129' HUP
|
||||
setsid --wait "$LEPTON" waitforexitandrun -- "$DIR/app.apk" &
|
||||
# 9>&-: the lock is this launcher's alone; Lepton's tree mustn't keep it held.
|
||||
setsid --wait "$LEPTON" waitforexitandrun -- "$DIR/app.apk" 9>&- &
|
||||
child=$!
|
||||
rc=0
|
||||
wait "$child" || rc=$?
|
||||
|
||||
@@ -24,7 +24,7 @@ spec.loader.exec_module(shortcuts)
|
||||
|
||||
@unittest.skipIf(os.name == 'nt', 'POSIX launcher')
|
||||
class LauncherTests(unittest.TestCase):
|
||||
def exercise(self, terminate, sig=signal.SIGTERM, blocked=None):
|
||||
def exercise(self, terminate, sig=signal.SIGTERM, blocked=None, orphan=False):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp)
|
||||
app = d / 'Applications/Android/org.test.app'
|
||||
@@ -44,6 +44,7 @@ class LauncherTests(unittest.TestCase):
|
||||
'assert os.environ["SteamAppId"] == "2800000001"\n'
|
||||
'assert os.environ["LEPTON_ENV_SteamAppId"] == "3346865537"\n'
|
||||
'Path(os.environ["HOME"],"started").write_text(str(os.getpid()))\n'
|
||||
'try:\n os.fstat(9); Path(os.environ["HOME"],"inherited-lock").touch()\nexcept OSError: pass\n'
|
||||
+ ('time.sleep(30)\n' if terminate else 'raise SystemExit(23)\n'))
|
||||
script(bin_dir / 'setsid', 'import os,sys\nos.setsid()\nos.execv(sys.argv[2],sys.argv[2:])\n')
|
||||
script(bin_dir / 'flock', 'import os\nraise SystemExit(1 if os.environ.get("TEST_LOCKED") else 0)\n') # lock semantics belong to Linux; no flock on macOS
|
||||
@@ -54,6 +55,8 @@ class LauncherTests(unittest.TestCase):
|
||||
env = {**os.environ, 'HOME': str(d), 'PATH': str(bin_dir) + os.pathsep + os.environ['PATH']}
|
||||
if blocked:
|
||||
env['TEST_' + blocked] = '1'
|
||||
if orphan:
|
||||
env['TEST_RUNNING'] = '1'
|
||||
saved = d / '.local/share/Steam/steamapps/compatdata/2800000001/internal/save'
|
||||
saved.parent.mkdir(parents=True)
|
||||
saved.write_text('saved game')
|
||||
@@ -70,12 +73,14 @@ class LauncherTests(unittest.TestCase):
|
||||
while not (d / 'started').exists() and proc.poll() is None and time.monotonic() < deadline:
|
||||
time.sleep(.02)
|
||||
self.assertTrue((d / 'started').exists(), 'launcher did not start Lepton')
|
||||
self.assertFalse((d / 'inherited-lock').exists(), 'Lepton inherited the launch lock')
|
||||
if terminate:
|
||||
self.assertIsNone(proc.poll(), 'Steam-tracked wrapper exited during the session')
|
||||
proc.send_signal(sig)
|
||||
_, err = proc.communicate(timeout=5)
|
||||
calls = (d / 'podman-calls').read_text() if (d / 'podman-calls').exists() else ''
|
||||
self.assertIn('stop -t 5 lepton-steamlaunch-2800000001', calls, err.decode())
|
||||
self.assertEqual(calls.count('stop -t 5'), 2 if orphan else 1)
|
||||
self.assertEqual(proc.returncode, 128 + sig if terminate else 23)
|
||||
self.assertEqual(saved.read_text(), 'saved game')
|
||||
self.assertTrue((app / 'app.apk').exists())
|
||||
@@ -98,9 +103,11 @@ class LauncherTests(unittest.TestCase):
|
||||
self.exercise(True, sig)
|
||||
|
||||
def test_duplicate_launch_leaves_existing_session_alone(self):
|
||||
for blocked in ('LOCKED', 'RUNNING'):
|
||||
with self.subTest(blocked=blocked):
|
||||
self.exercise(False, blocked=blocked)
|
||||
self.exercise(False, blocked='LOCKED')
|
||||
|
||||
def test_orphaned_container_is_stopped_and_play_proceeds(self):
|
||||
# Container running but the lock free: its launcher was SIGKILLed.
|
||||
self.exercise(False, orphan=True)
|
||||
|
||||
def test_normal_exit_cleans_container_and_keeps_exit_code(self):
|
||||
self.exercise(False)
|
||||
|
||||
Reference in new issue
Block a user