Files

341 lines
17 KiB
Python

"""Shared deployment: no APK edits or renderer changes; games opt in through their recipe (frame.hw_video_decode)."""
import base64
import hashlib
import importlib.util
import io
import json
import sys
import threading
import types
import zipfile
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
def agent_module(monkeypatch, tmp_path):
if sys.platform == "win32":
monkeypatch.setitem(sys.modules, "fcntl", types.SimpleNamespace(flock=lambda *_: None, LOCK_EX=2))
spec = importlib.util.spec_from_file_location("shared_codec_agent", ROOT / "agent/frameport_agent.py")
agent = importlib.util.module_from_spec(spec)
spec.loader.exec_module(agent)
monkeypatch.setattr(agent, "VIDEO_CODEC_DIR", str(tmp_path / "codec"))
monkeypatch.setattr(agent, "ANCHORS", str(tmp_path / "apps"))
return agent
def bundle(revision=2, corrupt=False):
files = {"libstagefrighthw.so": b"plugin", "media_codecs_frameport.xml": b"xml",
"podman.py": b"wrapper", "COPYING.FFmpeg": b"license"}
manifest = {"revision": revision, "runtime_sha256": "runtime",
"files": {n: hashlib.sha256(data).hexdigest() for n, data in files.items()}}
raw = json.dumps(manifest).encode()
data = io.BytesIO()
with zipfile.ZipFile(data, "w") as z:
z.writestr("manifest.json", raw)
for name, payload in files.items():
z.writestr(name, b"broken" if corrupt and name == "libstagefrighthw.so" else payload)
return {"bundle": base64.b64encode(data.getvalue()).decode(), "digest": hashlib.sha256(raw).hexdigest()}
@pytest.mark.skipif(sys.platform == "win32", reason="agent publication uses Linux symlinks")
def test_shared_deployment_without_any_apk_and_no_downgrade(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
current = tmp_path / "codec/current"
replace = agent.os.replace
activated = []
def activate(source, destination):
if Path(destination) == current:
version = Path(source).resolve()
assert json.loads((version / "deployment.json").read_text())["scope"] == "shared"
assert (version / "bin/podman").read_bytes() == b"wrapper"
assert (version / "libstagefrighthw.so").read_bytes() == b"plugin"
assert not current.exists() # no partially published first version
activated.append(version)
replace(source, destination)
monkeypatch.setattr(agent.os, "replace", activate)
first = bundle(revision=3)
assert agent.cmd_install_video_codec(first)["digest"] == first["digest"]
assert len(activated) == 1
assert agent.cmd_video_codec_status({}) == {"digest": first["digest"], "revision": 3, "disabled": False}
assert agent.cmd_install_video_codec(first)["digest"] == first["digest"]
assert agent.cmd_install_video_codec(bundle(revision=2))["digest"] == first["digest"]
assert len(activated) == 1
with pytest.raises(agent.AgentError, match="checksum mismatch"):
agent.cmd_install_video_codec(bundle(revision=4, corrupt=True))
assert agent.cmd_video_codec_status({})["digest"] == first["digest"]
(current / "libstagefrighthw.so").write_bytes(b"bad on disk")
assert agent.cmd_video_codec_status({}) == {"disabled": False}
@pytest.mark.skipif(sys.platform == "win32", reason="agent publication uses Linux symlinks")
def test_superseded_versions_are_pruned(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
versions = tmp_path / "codec/versions"
installed = [agent.cmd_install_video_codec(bundle(revision=r))["digest"] for r in (3, 4)]
(versions / ".install-stale").mkdir()
(versions / "notes.txt").write_text("not a codec version")
installed.append(agent.cmd_install_video_codec(bundle(revision=5))["digest"])
# The active version and the one it replaced (in-flight launches) remain.
assert {p.name for p in versions.iterdir()} == {installed[1], installed[2], "notes.txt"}
assert agent.cmd_video_codec_status({})["digest"] == installed[2]
def test_corrupt_bundle_never_activates(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
with pytest.raises(agent.AgentError, match="checksum mismatch"):
agent.cmd_install_video_codec(bundle(corrupt=True))
assert not (tmp_path / "codec").exists()
LEPTON = "export LEPTON_ENV_FRAMEBRIDGE_CONFIG=\"$app_dir/settings.conf\"\n{line}\nchild=''\nsetsid /lepton start\n"
def anchor(agent, tmp_path, name, launcher, patches=(), legacy=False, dep_extra=None):
directory = tmp_path / "apps" / name
base = tmp_path / "games" / name
directory.mkdir(parents=True)
(base / "lepton-app").mkdir(parents=True)
(base / "lepton-app/game.apk").write_bytes(b"unchanged APK")
if legacy: # agent <= 70 extracted this game's codec from its APK
(base / "frameport-codec/bin").mkdir(parents=True)
(base / "frameport-codec/bin/podman").write_text("old wrapper")
dep = {"package": name, "base": str(base), "recipe": {"patches": list(patches)}, **(dep_extra or {})}
(directory / "deployment.json").write_text(json.dumps(dep))
(directory / "launch.sh").write_text(launcher)
return directory, base
def test_launchers_get_the_codec_line_only_for_games_with_the_patch(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
old, pr = agent.OLD_CODEC_LINES
plain, _ = anchor(agent, tmp_path, "com.any.game", LEPTON.format(line=old))
player, _ = anchor(agent, tmp_path, "cn.vr4p.oculus4xvrplayerov", LEPTON.format(line=pr),
patches=["frame.adapter", "frame.hw_video_decode"])
batman, batman_base = anchor(agent, tmp_path, "com.camouflaj.manta", LEPTON.format(line=old), legacy=True)
other_old, other_base = anchor(agent, tmp_path, "com.old.game", LEPTON.format(line=old))
(other_base / "frameport-codec").mkdir() # an APK without codec assets: agent 70 removed its wrapper
rift, _ = anchor(agent, tmp_path, "rift.game", "exec proton\n")
assert set(agent.upgrade_launchers()) == {"com.any.game", "cn.vr4p.oculus4xvrplayerov", "com.camouflaj.manta",
"com.old.game"}
for directory, want in ((plain, False), (player, True), (batman, True), (other_old, False)):
text = (directory / "launch.sh").read_text()
assert (agent.VIDEO_CODEC_LINE in text) is want, directory.name
assert not any(line in text for line in agent.OLD_CODEC_LINES)
assert text.endswith("child=''\nsetsid /lepton start\n")
assert "surface_native" not in text
assert (player / "launch.sh").read_text().count(agent.VIDEO_CODEC_LINE) == 1
assert (rift / "launch.sh").read_text() == "exec proton\n"
# Batman's choice is kept, so its old codec folder can go (ensure_host_fixes)
assert json.loads((batman / "deployment.json").read_text())["hw_video_decode"] is True
assert set(agent.remove_old_codec_dirs()) == {"com.camouflaj.manta", "com.old.game"}
assert not (batman_base / "frameport-codec").exists() and not (other_base / "frameport-codec").exists()
assert (batman_base / "lepton-app/game.apk").read_bytes() == b"unchanged APK"
assert agent.upgrade_launchers() == [] # idempotent, and Batman keeps the line without its old folder
assert agent.VIDEO_CODEC_LINE in (batman / "launch.sh").read_text()
# a reinstall without the patch (finalize writes hw_video_decode: false) drops the line
dep = json.loads((player / "deployment.json").read_text())
(player / "deployment.json").write_text(json.dumps(dict(dep, hw_video_decode=False)))
assert agent.upgrade_launchers() == ["cn.vr4p.oculus4xvrplayerov"]
assert agent.VIDEO_CODEC_LINE not in (player / "launch.sh").read_text()
def test_unconverted_old_codec_folder_stays(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
_, base = anchor(agent, tmp_path, "com.camouflaj.manta", LEPTON.format(line=agent.OLD_CODEC_LINES[0]),
legacy=True)
assert agent.remove_old_codec_dirs() == [] # its launcher hasn't been converted yet
assert (base / "frameport-codec/bin/podman").exists()
def test_old_codec_folder_goes_when_the_recipe_has_the_patch(monkeypatch, tmp_path):
"""Seen on the dev Frame: Batman's recipe already listed frame.hw_video_decode, so wants_hw_video answered from the
recipe and saved nothing; the old folder must still go once the launcher is converted."""
agent = agent_module(monkeypatch, tmp_path)
_, base = anchor(agent, tmp_path, "com.camouflaj.manta", LEPTON.format(line=agent.OLD_CODEC_LINES[0]),
patches=["frame.adapter", "frame.hw_video_decode"], legacy=True)
assert agent.upgrade_launchers() == ["com.camouflaj.manta"]
assert agent.remove_old_codec_dirs() == ["com.camouflaj.manta"]
assert not (base / "frameport-codec").exists()
def test_new_launchers_follow_the_recipe(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
for want in (True, False):
directory = tmp_path / f"apps/game{want}"
directory.mkdir(parents=True)
agent.write_launcher(str(directory), str(tmp_path / "base"), "com.example.game", "Game", 123,
"/lepton/lepton", {}, hw_video=want)
text = (directory / "launch.sh").read_text()
assert (agent.VIDEO_CODEC_LINE in text) is want
assert agent.set_codec_line(text, want) == text # upgrade_launchers leaves it as it is
@pytest.mark.skipif(sys.platform == "win32", reason="runs the launcher line in bash")
@pytest.mark.parametrize("case,expected", [("on", True), ("env", False), ("env0", True), ("flag", False),
("missing", False)])
def test_codec_line_switches(monkeypatch, tmp_path, case, expected):
"""The launcher line: the shared wrapper first on PATH unless switched off (FramePort's setting writes
video-codec/disabled; FRAMEPORT_NO_HW_VIDEO=1 in a game's Steam launch options) or not installed."""
import shutil
import subprocess
if not shutil.which("bash"):
pytest.skip("no bash")
agent = agent_module(monkeypatch, tmp_path)
home = tmp_path / "home"
codec = home / ".local/share/frameport/video-codec"
if case != "missing":
(codec / "versions/abc/bin").mkdir(parents=True)
(codec / "versions/abc/bin/podman").write_text("#!/bin/sh\n")
(codec / "versions/abc/bin/podman").chmod(0o755)
(codec / "current").symlink_to("versions/abc")
if case == "flag":
(codec / "disabled").write_text("off")
env = {"HOME": str(home), "PATH": "/usr/bin:/bin"}
if case.startswith("env"):
env["FRAMEPORT_NO_HW_VIDEO"] = "0" if case == "env0" else "1"
out = subprocess.run(["bash", "-euc", agent.VIDEO_CODEC_LINE + '\necho "$PATH"'], env=env, capture_output=True,
text=True, check=True).stdout.strip()
assert out.startswith(str(codec / "current/bin") + ":") is expected
assert out.endswith("/usr/bin:/bin")
def test_switch_writes_the_frame_wide_flag(monkeypatch, tmp_path):
agent = agent_module(monkeypatch, tmp_path)
assert agent.cmd_video_codec_switch({"enabled": False}) == {"disabled": True}
assert (tmp_path / "codec/disabled").exists()
assert agent.cmd_video_codec_switch({"enabled": True}) == {"disabled": False}
assert not (tmp_path / "codec/disabled").exists()
@pytest.mark.skipif(sys.platform == "win32", reason="agent publication uses Linux symlinks")
def test_same_revision_built_elsewhere_is_kept(monkeypatch, tmp_path):
"""Two PCs whose builds of one revision differ must not replace each other's codec at every connection."""
agent = agent_module(monkeypatch, tmp_path)
first = bundle(revision=7)
agent.cmd_install_video_codec(first)
other = bundle(revision=7)
data = io.BytesIO(base64.b64decode(other["bundle"]))
with zipfile.ZipFile(data) as z:
files = {n: z.read(n) for n in z.namelist()}
manifest = json.loads(files["manifest.json"])
manifest["build"] = "another PC"
files["manifest.json"] = json.dumps(manifest).encode()
rebuilt = io.BytesIO()
with zipfile.ZipFile(rebuilt, "w") as z:
for name, payload in files.items():
z.writestr(name, payload)
other = {"bundle": base64.b64encode(rebuilt.getvalue()).decode(),
"digest": hashlib.sha256(files["manifest.json"]).hexdigest()}
result = agent.cmd_install_video_codec(other)
assert result["kept"] and result["digest"] == first["digest"]
assert agent.cmd_video_codec_status({})["digest"] == first["digest"]
assert agent.cmd_install_video_codec(bundle(revision=8))["revision"] == 8 # a newer revision still installs
def test_connection_deploys_shared_codec_once_and_does_not_rebuild_games(monkeypatch):
from frameport.frame import connection
frame = connection.Frame.__new__(connection.Frame)
frame._video_codec_lock = threading.Lock()
frame._video_codec_digest = ""
calls = []
def remote(command, **args):
assert args.pop("ensure") is False # no ensure_agent recursion
calls.append(command)
if command == "video_codec_status":
return {}
with zipfile.ZipFile(io.BytesIO(base64.b64decode(args["bundle"]))) as z:
assert "manifest.json" in z.namelist()
assert not any(name.endswith(".apk") for name in z.namelist())
return {"digest": args["digest"]}
monkeypatch.setattr(frame, "agent", remote)
frame.ensure_video_codec()
frame.ensure_video_codec()
assert calls == ["video_codec_status", "install_video_codec"]
def test_newer_shared_codec_is_kept(monkeypatch):
from frameport.frame import connection
frame = connection.Frame.__new__(connection.Frame)
frame._video_codec_lock = threading.Lock()
frame._video_codec_digest = ""
calls = []
def remote(command, **_):
calls.append(command)
return {"digest": "newer-version", "revision": 999}
monkeypatch.setattr(frame, "agent", remote)
frame.ensure_video_codec()
assert calls == ["video_codec_status"]
def connection_frame(monkeypatch, replies):
from frameport.frame import connection
frame = connection.Frame.__new__(connection.Frame)
frame._video_codec_lock = threading.Lock()
frame._video_codec_digest = ""
calls = []
def remote(command, **args):
assert args.pop("ensure") is False
calls.append((command, args.get("enabled")))
reply = replies.get(command, {})
if isinstance(reply, Exception):
raise reply
return reply
monkeypatch.setattr(frame, "agent", remote)
return frame, calls
def test_failed_install_is_not_retried_on_this_connection(monkeypatch):
from frameport.frame import connection
frame, calls = connection_frame(monkeypatch, {"install_video_codec": connection.AgentFailed("disk full")})
with pytest.raises(connection.AgentFailed):
frame.ensure_video_codec()
frame.ensure_video_codec() # every agent call comes through here: no second ~6 MB upload
frame._ensure_video_codec_if_supported(b"AGENT_VERSION = 71\n")
assert [c for c, _ in calls] == ["video_codec_status", "install_video_codec"]
def test_same_revision_on_the_frame_is_kept(monkeypatch):
revision = json.loads((ROOT / "artifacts/hevc/manifest.json").read_text())["revision"]
frame, calls = connection_frame(monkeypatch, {"video_codec_status": {"digest": "built-elsewhere",
"revision": revision}})
frame.ensure_video_codec()
assert [c for c, _ in calls] == ["video_codec_status"]
frame, calls = connection_frame(monkeypatch, {"video_codec_status": {"digest": "older",
"revision": revision - 1}})
frame.ensure_video_codec()
assert [c for c, _ in calls] == ["video_codec_status", "install_video_codec"]
def test_setting_off_switches_the_frame_off_without_uploading(monkeypatch):
from frameport.core import library
library.set_setting("video.hw_decode", False)
replies = {"video_codec_status": {"disabled": False}, "video_codec_switch": {"disabled": True}}
frame, calls = connection_frame(monkeypatch, replies)
frame.ensure_video_codec()
assert calls == [("video_codec_status", None), ("video_codec_switch", False)]
# switched on again in Settings: applied at once (switch + install)
library.set_setting("video.hw_decode", True)
replies.update(video_codec_status={"disabled": True}, video_codec_switch={"disabled": False})
monkeypatch.setattr(frame, "ensure_agent", lambda: frame.ensure_video_codec())
calls.clear()
frame.apply_video_decoding()
assert [c for c, _ in calls] == ["video_codec_status", "video_codec_switch", "install_video_codec"]
assert calls[1] == ("video_codec_switch", True)