mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 01:00:18 +02:00
Sideloaded titles: use ids Steam's create-shortcut accepts
On the Frame, Steam refused to register titles whose id had a hyphen (fc-smoke-exe) with "missing/invalid arguments", and registered the same program as FCSmokeProbe (headset smoke test, 2026-09-27, BUILD_ID 20260922.6101926). Valve's client only allows ^[A-Za-z_][A-Za-z0-9_.]+$. title_id now makes ids of letters, digits and _, not starting with a digit, 2 to 64 long; new installs are checked against that, while titles already on the Frame are still listed, launched and removed. Steam's error text is trimmed before it's quoted, and the "install it again with Steam running" hint only follows a Steam-not-running error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
10bf18fd50
commit
ca2a991f03
4 files changed
+48
-19
No files matched your search
+10
-4
@@ -21,7 +21,8 @@ desktop app, which knows where a dropped folder lives; in a plain browser, zip
|
||||
it.) A dialog shows:
|
||||
|
||||
- **Name**: what Steam shows. Steam uses the title id as the name, so it's
|
||||
limited to letters, digits, `_` and `-`; the dialog shows the result.
|
||||
limited to letters, digits and `_`, and can't start with a digit; the
|
||||
dialog shows the result.
|
||||
- **Launches**: the program picked to start the game, with the other
|
||||
candidates in the list.
|
||||
- **Runtime**: picked from the program, see below. Windows programs can switch
|
||||
@@ -133,10 +134,15 @@ splits that string is **not checked**.
|
||||
with the same rule, because `scp -r` would follow a link out of the folder
|
||||
and upload whatever it points at.
|
||||
- Installs run one at a time, and Remove is refused while one runs.
|
||||
- The title id is limited to `[A-Za-z0-9_-]`, at most 64 characters. Valve's
|
||||
scripts pass it to a shell (`steamos-delete` runs `rm -r` on it). Valve's
|
||||
- The title id is limited to letters, digits and `_`, doesn't start with a
|
||||
digit (one that would gets `_` in front), and is 2 to 64 characters. That's
|
||||
what Steam's `create-shortcut` accepts: on the Frame it refused
|
||||
`fc-smoke-exe` with `missing/invalid arguments` and registered the same
|
||||
program as `FCSmokeProbe` (2026-09-27, BUILD_ID 20260922.6101926), and
|
||||
Valve's client only allows `^[A-Za-z_][A-Za-z0-9_.]+$`. Valve's scripts
|
||||
also pass the id to a shell (`steamos-delete` runs `rm -r` on it). Valve's
|
||||
reserved sideload names (`steam`, `steamvr`, and their `deckard` forms,
|
||||
which would replace the Steam client itself) get `-game` added.
|
||||
which would replace the Steam client itself) get `_game` added.
|
||||
- Nothing needs `sudo`; everything goes to your home folder on the Frame.
|
||||
- In the app, a dropped folder is read from its local path by the app's own
|
||||
server, which only accepts requests from its own page (see
|
||||
|
||||
@@ -347,14 +347,26 @@ class Zips(unittest.TestCase):
|
||||
class Names(unittest.TestCase):
|
||||
def test_title_id(self):
|
||||
self.assertEqual(frame_titles.title_id('Hollow Knight: Silksong!'), 'Hollow_Knight_Silksong')
|
||||
self.assertEqual(frame_titles.title_id('steam'), 'steam-game') # Valve's reserved sideload names
|
||||
self.assertEqual(frame_titles.title_id('steam'), 'steam_game') # Valve's reserved sideload names
|
||||
self.assertEqual(frame_titles.title_id('Devkit Steam'), 'Devkit_Steam')
|
||||
self.assertEqual(frame_titles.title_id('devkit-steam'), 'devkit-steam-game') # the trampoline file
|
||||
self.assertEqual(frame_titles.title_id('--rm -rf /'), 'rm_-rf')
|
||||
self.assertEqual(frame_titles.title_id('--rm -rf /'), 'rm_rf')
|
||||
self.assertEqual(len(frame_titles.title_id('x' * 200)), 64)
|
||||
with self.assertRaises(FrameError):
|
||||
frame_titles.title_id('!!!')
|
||||
|
||||
def test_title_id_is_one_steam_accepts(self):
|
||||
# The Frame's Steam refused "fc-smoke-exe" ("missing/invalid arguments") and took
|
||||
# "FCSmokeProbe" (2026-09-27): Valve's client allows ^[A-Za-z_][A-Za-z0-9_.]+$ only.
|
||||
self.assertEqual(frame_titles.title_id('Half-Life 2'), 'Half_Life_2')
|
||||
self.assertEqual(frame_titles.title_id('fc-smoke-exe'), 'fc_smoke_exe')
|
||||
self.assertEqual(frame_titles.title_id('2048'), '_2048')
|
||||
self.assertEqual(frame_titles.title_id('X'), 'X_game')
|
||||
self.assertEqual(frame_titles.title_id('devkit-steam'), 'devkit_steam')
|
||||
for name in ('Half-Life 2', '2048', 'X', 'steam', 'a' * 90, 'Ünïcödé game', '9' * 70):
|
||||
gid = frame_titles.title_id(name)
|
||||
self.assertRegex(gid, r'^[A-Za-z_][A-Za-z0-9_.]+$', name)
|
||||
self.assertTrue(frame_titles.NEW_ID_RE.match(gid) and frame_titles.ID_RE.match(gid), gid)
|
||||
|
||||
def test_display_name(self):
|
||||
self.assertEqual(frame_titles.display_name('MyGame-linux-arm64.zip'), 'MyGame')
|
||||
self.assertEqual(frame_titles.display_name('Portal 2.zip'), 'Portal 2')
|
||||
|
||||
+18
-9
@@ -35,7 +35,13 @@ PY = 'python3 ~/' + UTILS + '/'
|
||||
# devkit-steam is the trampoline file that switches SteamOS to a sideloaded client
|
||||
# (select_steam.sh); a folder there breaks Valve's devkit tools.
|
||||
RESERVED_IDS = ('steam', 'steamdeckard', 'steamvr', 'steamvrdeckard', 'devkit-steam')
|
||||
ID_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$')
|
||||
# Any title folder on the Frame we'll list, launch or remove: safe in a shell and a path.
|
||||
ID_RE = re.compile(r'^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$')
|
||||
# What a new install may use. Steam's create-shortcut refused "fc-smoke-exe" with
|
||||
# "missing/invalid arguments" and took the same program as "FCSmokeProbe" (headset
|
||||
# smoke test, 2026-09-27, build 20260922.6101926); Valve's client only allows
|
||||
# GAMEID_ALLOWED_PATTERN, ^[A-Za-z_][A-Za-z0-9_.]+$ (devkit_client/gui2). No dots here.
|
||||
NEW_ID_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]{1,63}$')
|
||||
DIR_RE = re.compile(r'^/[A-Za-z0-9_./-]+$')
|
||||
|
||||
# Zip limits: well above any real game, well below a zip bomb.
|
||||
@@ -124,13 +130,15 @@ def _pe(f, head):
|
||||
|
||||
|
||||
def title_id(name):
|
||||
"""The Devkit Game id Steam shows as the title's name: [A-Za-z0-9_-], at most 64."""
|
||||
s = re.sub(r'[^A-Za-z0-9_-]+', '_', str(name or '').strip())
|
||||
s = re.sub(r'_+', '_', s).strip('_-')[:64].strip('_-')
|
||||
"""The Devkit Game id Steam shows as the title's name, in the form Steam accepts
|
||||
(NEW_ID_RE): letters, digits and _, not starting with a digit, 2 to 64 long."""
|
||||
s = re.sub(r'[^A-Za-z0-9]+', '_', str(name or '').strip()).strip('_')[:64].strip('_')
|
||||
if not s:
|
||||
raise FrameError('the title needs a name with some letters or digits')
|
||||
if s.lower() in RESERVED_IDS:
|
||||
s += '-game'
|
||||
if s[0].isdigit():
|
||||
s = '_' + s[:63]
|
||||
if len(s) < 2 or s.lower() in RESERVED_IDS:
|
||||
s += '_game'
|
||||
return s
|
||||
|
||||
|
||||
@@ -653,7 +661,7 @@ def install_plan(plan, name=None, exe=None, runtime=None, progress=None):
|
||||
|
||||
def _install(plan, step):
|
||||
gid = plan['id']
|
||||
if not ID_RE.match(gid) or gid.lower() in RESERVED_IDS:
|
||||
if not NEW_ID_RE.match(gid) or gid.lower() in RESERVED_IDS:
|
||||
raise FrameError(f'bad title id {gid!r}')
|
||||
step("Syncing Valve's devkit tools to the Frame", 0.02)
|
||||
ensure_utils()
|
||||
@@ -683,8 +691,9 @@ def _install(plan, step):
|
||||
ssh(f'cat > {GAMES}/{gid}-framecontrol.json', input=json.dumps(meta, indent=1), timeout=30)
|
||||
registered = True # the files stay: Steam registers them once it's running
|
||||
if 'error' in reply:
|
||||
raise FrameError(f"Uploaded, but Steam didn't register it: {reply['error']}. "
|
||||
"With Steam running on the Frame, install it again.")
|
||||
err = str(reply['error']).strip().rstrip('.')
|
||||
hint = ' With Steam running on the Frame, install it again.' if 'not running' in err else ''
|
||||
raise FrameError(f"Uploaded, but Steam didn't register it: {err}.{hint}")
|
||||
step('Done', 1.0)
|
||||
meta.update(runtime_label=RUNTIMES[plan['runtime']]['label'], steam=str(reply.get('success', '')).strip())
|
||||
return meta
|
||||
|
||||
+5
-3
@@ -1433,9 +1433,11 @@ async function sendFiles(files, dirs = new Set()) {
|
||||
// ---- sideloaded titles (a Linux or Windows build as a Steam Devkit Game) ----
|
||||
const RESERVED_IDS = ["steam", "steamdeckard", "steamvr", "steamvrdeckard", "devkit-steam"];
|
||||
function titleId(name) { // mirrors frame_titles.title_id: the name Steam shows
|
||||
const trim = x => x.replace(/^[_-]+|[_-]+$/g, "");
|
||||
let s = trim(trim(String(name).trim().replace(/[^A-Za-z0-9_-]+/g, "_").replace(/_+/g, "_")).slice(0, 64));
|
||||
if (RESERVED_IDS.includes(s.toLowerCase())) s += "-game";
|
||||
const trim = x => x.replace(/^_+|_+$/g, "");
|
||||
let s = trim(trim(String(name).trim().replace(/[^A-Za-z0-9]+/g, "_")).slice(0, 64));
|
||||
if (!s) return s;
|
||||
if (/^[0-9]/.test(s)) s = "_" + s.slice(0, 63);
|
||||
if (s.length < 2 || RESERVED_IDS.includes(s.toLowerCase())) s += "_game";
|
||||
return s;
|
||||
}
|
||||
const ARCH = { arm64: "ARM64", x86_64: "x86-64", x86: "32-bit x86" };
|
||||
|
||||
Reference in new issue
Block a user