mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-25 11:01:42 +01:00
* Add sandboxed local Frame Control extensions * Add experimental Decky Loader manager for Steam Frame * ci: verify Decky PR against main --------- Co-authored-by: SAPHID <saphid@users.noreply.github.com>
95 lines
4.4 KiB
Python
95 lines
4.4 KiB
Python
"""Decky release validation and the removable user-service transaction."""
|
|
import sandbox # noqa: F401
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
import secrets
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'ui'))
|
|
import frame_decky
|
|
|
|
|
|
def arm_binary(fill=b'A'):
|
|
data = bytearray(b'\x7fELF' + fill * 2044)
|
|
data[18:20] = (183).to_bytes(2, 'little')
|
|
return bytes(data)
|
|
|
|
|
|
class DeckyRelease(unittest.TestCase):
|
|
def test_official_arm_release_digest_is_checked(self):
|
|
data = arm_binary()
|
|
digest = hashlib.sha256(data).hexdigest()
|
|
url = f'https://github.com/{frame_decky.REPO}/releases/download/v3.3.0/PluginLoader-arm64'
|
|
info = {'tag_name':'v3.3.0', 'assets':[{'name':'PluginLoader-arm64', 'size':len(data),
|
|
'digest':'sha256:'+digest, 'browser_download_url':url}]}
|
|
with patch.object(frame_decky.urllib.request, 'urlopen', side_effect=[io.BytesIO(json.dumps(info).encode()), io.BytesIO(data)]):
|
|
self.assertEqual(frame_decky.release_binary(), ('v3.3.0', digest, data))
|
|
info['assets'][0]['digest'] = 'sha256:' + '0'*64
|
|
with patch.object(frame_decky.urllib.request, 'urlopen', side_effect=[io.BytesIO(json.dumps(info).encode()), io.BytesIO(data)]):
|
|
with self.assertRaisesRegex(ValueError, 'digest'):
|
|
frame_decky.release_binary()
|
|
|
|
def test_install_rejects_incompatible_headset_before_download(self):
|
|
state = {'arch':'x86_64','variant':'vr','cef':True,'managed':False,'upstreamInstall':False}
|
|
with patch.object(frame_decky, 'release_binary') as download:
|
|
with self.assertRaisesRegex(ValueError, 'Steam Frame'):
|
|
frame_decky.change(lambda *a, **k: json.dumps(state), 'install')
|
|
download.assert_not_called()
|
|
|
|
|
|
@unittest.skipIf(os.name == 'nt', 'remote service script runs on Linux')
|
|
class DeckyServiceScript(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.tmp.cleanup)
|
|
home = Path(self.tmp.name)
|
|
fake = home / 'systemctl'
|
|
fake.write_text('#!/bin/sh\nif [ "$FRAME_DECKY_TEST_FAIL" = "1" ] && [ "$2" = "enable" ]; then echo blocked >&2; exit 1; fi\nexit 0\n')
|
|
fake.chmod(0o700)
|
|
self.env = {**os.environ, 'HOME': self.tmp.name, 'PATH': self.tmp.name + os.pathsep + os.environ['PATH']}
|
|
self.base = home / '.local/share/frame-control/decky'
|
|
|
|
def apply(self, action, data=None, version='v3.3.0', fail=False):
|
|
token = secrets.token_hex(12)
|
|
stage = Path('/tmp/frame-control-decky-' + token)
|
|
if data is not None:
|
|
stage.write_bytes(data)
|
|
digest = hashlib.sha256(data or b'').hexdigest()
|
|
env = {**self.env, 'FRAME_DECKY_TEST_FAIL': '1' if fail else '0'}
|
|
try:
|
|
return subprocess.run([sys.executable, '-', action, token, digest, version],
|
|
input=frame_decky.APPLY, capture_output=True, text=True, env=env, timeout=15)
|
|
finally:
|
|
stage.unlink(missing_ok=True)
|
|
|
|
def test_install_update_rollback_and_remove(self):
|
|
first = arm_binary(b'A')
|
|
result = self.apply('install', first)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual((self.base / 'PluginLoader').read_bytes(), first)
|
|
receipt = self.base / '.frame-control/receipt.json'
|
|
self.assertEqual(json.loads(receipt.read_text())['version'], 'v3.3.0')
|
|
self.assertTrue((Path(self.tmp.name) / '.config/systemd/user/frame-control-decky.service').is_file())
|
|
|
|
second = arm_binary(b'B')
|
|
result = self.apply('update', second, version='v3.3.1', fail=True)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertEqual((self.base / 'PluginLoader').read_bytes(), first)
|
|
self.assertEqual(json.loads(receipt.read_text())['version'], 'v3.3.0')
|
|
|
|
result = self.apply('update', second, version='v3.3.1')
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual((self.base / 'PluginLoader').read_bytes(), second)
|
|
result = self.apply('remove')
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertFalse((self.base / 'PluginLoader').exists())
|
|
self.assertFalse(receipt.exists())
|
|
self.assertTrue((self.base / 'plugins').is_dir())
|