mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 03:00:18 +02:00
Sideload Linux and Windows builds as Steam Devkit Games
Dropping a game's .zip, folder or .exe on Send to Frame now adds it to the headset's Steam library through Valve's SteamOS Devkit title path, with the runtime picked from the program's header: Windows PE -> Proton Experimental (steam_play=1), aarch64 ELF -> SteamLinuxRuntime_4-arm64, x86-64 ELF -> SteamLinuxRuntime_4 (through FEX). Other architectures are refused. - frame/devkit-utils: Valve's devkit-utils vendored unmodified (MIT, steamos-devkit v0.20260925.1), synced to ~/devkit-utils by stamp, bundled in the app and compiled in CI. - ui/frame_titles.py: inspect (safe unzip, ELF/PE classification, launch target ranking), install(path, name=None, exe=None, runtime=None, progress=None), list, launch, remove, plus a CLI. - ui/server.py: /api/titles (inspect/install/discard/launch/remove), /api/titles/job progress, and an upload mode 'title'. - ui/index.html: confirm dialog (name, launch target, runtime), install progress, and a Sideloaded titles list with Launch and Remove. The app's preload passes a dropped folder's path. - tests and docs/sideloading.md. Device-side behaviour is inferred from Valve's source; the headset was offline, so none of it has been checked on a Frame yet. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1a0e54d8bd
commit
6c4d387ef3
26 files changed
+3191
-26
No files matched your search
+35
-1
@@ -6,14 +6,17 @@ request guards and input validation, which all run before any SSH call.
|
||||
Run: python3 -m unittest discover -s tests
|
||||
"""
|
||||
import http.client
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
@@ -54,7 +57,7 @@ class ServerGuards(unittest.TestCase):
|
||||
@classmethod
|
||||
def request(cls, method, path, body=None, headers=None):
|
||||
conn = http.client.HTTPConnection("127.0.0.1", cls.port, timeout=10)
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
data = body if isinstance(body, bytes) else json.dumps(body).encode() if body is not None else None
|
||||
conn.request(method, path, body=data, headers=headers or {})
|
||||
r = conn.getresponse()
|
||||
payload = r.read()
|
||||
@@ -130,6 +133,37 @@ class ServerGuards(unittest.TestCase):
|
||||
status, _ = self.post("/api/launch", ["not", "an", "object"])
|
||||
self.assertEqual(status, 400)
|
||||
|
||||
def test_title_upload_is_inspected_then_discarded(self):
|
||||
# A zip holding a Windows x86-64 program: inspected locally, no SSH until install.
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w") as z:
|
||||
z.writestr("Tiny Game/Tiny Game.exe",
|
||||
b"MZ" + b"\0" * 0x3A + struct.pack("<I", 0x40) + b"PE\0\0" + struct.pack("<HHIIIHH", 0x8664, 1, 0, 0, 0, 0xF0, 0x22))
|
||||
status, _, payload = self.request("POST", "/api/upload", buf.getvalue(),
|
||||
{"X-Frame-UI": "1", "X-Mode": "title", "X-Filename": quote("Tiny Game-win64.zip")})
|
||||
r = json.loads(payload)
|
||||
self.assertEqual(status, 200, r)
|
||||
self.assertEqual((r["plan"]["id"], r["plan"]["target"], r["plan"]["runtime"]),
|
||||
("Tiny_Game", "Tiny Game.exe", "proton-experimental"))
|
||||
self.assertNotIn("root", r["plan"])
|
||||
self.assertEqual(self.post("/api/titles", {"action": "discard", "token": r["token"]})[0], 200)
|
||||
self.assertEqual(self.post("/api/titles", {"action": "install", "token": r["token"]})[0], 400)
|
||||
|
||||
def test_title_input_validation(self):
|
||||
status, _, _ = self.request("POST", "/api/upload", b"not a zip",
|
||||
{"X-Frame-UI": "1", "X-Mode": "title", "X-Filename": "x.zip"})
|
||||
self.assertEqual(status, 400)
|
||||
for body in ({"action": "inspect", "path": "relative/game.zip"},
|
||||
{"action": "inspect", "path": "/nonexistent/frame-control/game.zip"},
|
||||
{"action": "install", "token": "nope"},
|
||||
{"action": "launch", "id": "x; rm -rf ~"},
|
||||
{"action": "remove", "id": "../etc"},
|
||||
{"action": "explode"}):
|
||||
status, payload = self.post("/api/titles", body)
|
||||
self.assertEqual(status, 400, f"{body} -> {payload}")
|
||||
self.assertEqual(self.request("GET", "/api/titles/job?token=nope", headers={"X-Frame-UI": "1"})[0], 404)
|
||||
self.assertEqual(self.request("POST", "/api/titles", {"action": "list"})[0], 403)
|
||||
|
||||
def test_unknown_routes(self):
|
||||
self.assertEqual(self.request("GET", "/nope")[0], 404)
|
||||
self.assertEqual(self.post("/api/nope", {})[0], 404)
|
||||
|
||||
Reference in new issue
Block a user