mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 13:00:33 +01:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fc5b83873 | ||
|
|
d96330554a | ||
|
|
385ae8290b | ||
|
|
858edaf7eb | ||
|
|
f5213b7c44 | ||
|
|
0219cd10f0 | ||
|
|
b0c0599f45 |
No files matched your search
@@ -0,0 +1,33 @@
|
||||
# Tailscale Control read-only backend and Decky PATH, 26 October 2026
|
||||
|
||||
The audited archive was official Tailscale Control 0.1.3, store ID 43,
|
||||
SHA-256 `06411711b27101d438339000da3d91202d682862710b02ef3ddf2462e642448a`.
|
||||
Earlier local Chromium evidence showed its settings panel. This check focused
|
||||
on the Frame's CLI lookup; it did not run a live Framey panel RPC.
|
||||
|
||||
Each real Frame check acquired `/tmp/frame-test.lock` atomically with its own
|
||||
random owner, read SteamVR activity and proximity, and released only that
|
||||
owner's lock. The headset was in standby. The installed Tailscale CLI wrapper
|
||||
resides in `~/.local/bin`, which the systemd user manager did not put in the
|
||||
Decky service's inherited `PATH`.
|
||||
|
||||
| Check | Result |
|
||||
| --- | --- |
|
||||
| Exact official backend with the current service PATH | Three read-only status methods returned `None`; `tailscale` was not found. |
|
||||
| Same backend in a disposable process with `~/.local/bin` prepended | The read-only methods returned active state, two exit-node entries and 16 device entries. |
|
||||
| Bounded transient systemd user unit with the proposed service PATH | Found `tailscale` and completed `tailscale status`. The unit ended inactive, PID 0, `LoadState=not-found`. |
|
||||
|
||||
The Frame Control managed Decky unit now sets `PATH` to `~/.local/bin` plus
|
||||
standard system directories when installed or updated. The install transaction
|
||||
rolls its prior unit back on failure. No Tailscale `up`, `down`, routing or
|
||||
exit-node change was run. No persistent test service, headset wake, controller
|
||||
input or settings write was used.
|
||||
|
||||
This supports CLI discovery and read-only backend data on the real Frame. It
|
||||
does not prove the production Framey-to-Decky RPC, connection controls, QR
|
||||
asset, worn panel visibility or physical controller usability. Tailscale
|
||||
Control remains **partial** in the digest-scoped catalog.
|
||||
|
||||
A later independent hourly read-only check found SteamVR activity 3
|
||||
(standby), proximity raw 3.0625 and battery 93%. It released its owned lock;
|
||||
no worn test was possible and the hourly schedule remains enabled.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Read-only Framey activity gate, 26 October 2026
|
||||
|
||||
At 11:12 UTC, a read-only probe acquired `/tmp/frame-test.lock` by atomic
|
||||
`mkdir` with a random owner token before reading the Frame. SteamVR
|
||||
returned two `vrcmd --stats` rows: performance metrics first and the status
|
||||
row second. The status row reported activity level **1**. The original
|
||||
`framey-worn-check.py` looked only at row zero and reported unknown activity;
|
||||
`framey-vr-check.py` used the same row-zero assumption for its wake gate. The
|
||||
fixed gates select the unique status row and fail closed if it is missing,
|
||||
duplicated or malformed.
|
||||
|
||||
The first locked read found 90% battery, proximity raw 222.25, no active
|
||||
managed headset panel service and no managed Decky plugin. A subsequent locked
|
||||
read found activity 1 and proximity raw 223.625. No reliable wearer
|
||||
confirmation was available. No wake, service install, settings write or input
|
||||
was attempted.
|
||||
|
||||
While activity was 1, a separate owned-lock read captured four seconds from
|
||||
`/dev/video99` into a private 640×360 H.264 MP4. It decoded successfully and
|
||||
contained active video imagery. The inspected sample did **not** establish
|
||||
room passthrough, Frame Control panel visibility, ProtonDB Badges UI or
|
||||
controller interaction. The private recording and decoded frame were removed
|
||||
after inspection; no room or media images are committed. Each read released
|
||||
only its owner-token lock. The hourly worn-check schedule remains enabled.
|
||||
|
||||
The local focused safety suite covers a metrics row before the status row,
|
||||
awake activity, ambiguous/missing status and foreign-lock refusal. A genuine
|
||||
worn visual and physical-controller check remains outstanding.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Interactive Framey worn-check proximity gate, 26 October 2026
|
||||
|
||||
The interactive `scripts/framey-worn-check.py` already required an explicit
|
||||
operator wear confirmation and a unique `/tmp/frame-test.lock` owner before
|
||||
reading SteamVR activity. It did not read proximity. This change makes its
|
||||
locked preflight read the raw proximity sensor and stop before its interaction
|
||||
window when the reading is missing or invalid. It checks activity, proximity
|
||||
and battery again before input. The same owned-lock cleanup applies on failure.
|
||||
|
||||
The focused local gate suite passes 9/9 tests, including missing and nonfinite
|
||||
proximity, proximity disappearing between reads, metrics preceding the SteamVR status row,
|
||||
standby, low battery, no operator confirmation and a foreign lock. No headset
|
||||
input was sent for this change. A separate owner-locked read-only Frame check
|
||||
found SteamVR activity 3 (standby), proximity raw 3.0625 and battery 93%; it
|
||||
released its lock without a worn test.
|
||||
|
||||
The raw proximity value is not calibrated as proof of wear. The operator's
|
||||
in-person confirmation remains required; awake activity, proximity and trusted
|
||||
synthetic input alone do not prove wearer-visible pixels or physical controller
|
||||
usability. The hourly worn-check schedule remains enabled.
|
||||
@@ -59,7 +59,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 37: Discord Status 1.5.0 | 1 / 2 | partial | Disconnected panel renders; running-activity construction accepts a synthetic game in local Chromium. Exact official is_connected returned false in real Decky 3.3.0 ARM64. Empty live running-list reads and lifetime registration/cleanup passed. Real game transitions, Discord shortcut detection/connection/activity, local custom artwork and worn VR remain unverified/unsupported. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 38: Volume Mixer 1.2.1-1 | legacy / legacy | works | Per-app stream discovery and slider/keyboard volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, with independent Pulse readback of a silent test stream. Bluetooth playback, audible output and worn-VR controls unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 40: Controller Tools 2.0.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: ControllerTools/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
|
||||
| 43: Tailscale Control 0.1.3 | legacy / legacy | partial | Settings render locally after the SuspensefulImage fallback. Tailscale daemon/state, connection and exit-node changes unverified; bundled localhost QR asset remains unavailable. No network settings were changed. | static archive, local browser, fake-Frame |
|
||||
| 43: Tailscale Control 0.1.3 | legacy / legacy | partial | Settings render locally after the SuspensefulImage fallback. On the real Frame, exact official read-only backend status calls returned live state, exit-node and device entries when ~/.local/bin was in PATH; a transient user unit found the CLI with the proposed managed-service PATH. Live Framey Decky RPC, connection/exit-node changes and worn VR remain unverified; the bundled localhost QR asset is unavailable. No network settings were changed. | static archive, local browser, fake-Frame, real Frame read-only backend |
|
||||
| 44: DeckMTP 1.0.5 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: DeckMTP/bin/umtprd. | static archive, local browser, fake-Frame |
|
||||
| 46: Steamback 1.0.0-1 | legacy / legacy | partial | Panel opens using the real Frame current-user/library snapshot. Account setup and empty save-list reads pass through actual Decky 3.3.0 amd64 in isolation. ARM64 backend, supported-save discovery, automatic backups and restore remain unverified; no backup/restore ran. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 48: Quick Launch 1.2.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
|
||||
@@ -112,8 +112,10 @@ the same-plugin navigation regression without a headset.
|
||||
interaction window, use the physical controller to open the sample and
|
||||
toggle **Example setting**. Confirm the laser aims where expected and the
|
||||
script records a fresh input/panel event and visible stereo output. The flag
|
||||
is the operator’s statement; sensor activity and trusted pointers can also
|
||||
be produced synthetically and cannot identify controller hardware.
|
||||
is the operator’s statement. The script requires valid locked SteamVR activity
|
||||
and proximity readings before input; proximity alone cannot establish wear.
|
||||
Sensor activity and trusted pointers can also be produced synthetically and
|
||||
cannot identify controller hardware.
|
||||
3. **1:15–2:15:** Use the controller to open Settings, return to Panel, invoke
|
||||
**Read saved setting**, close/reopen the plugin, and confirm the value
|
||||
remains. Try controller scroll, a slider and a harmless text field in an
|
||||
|
||||
@@ -90,6 +90,15 @@ def settings(state):
|
||||
return pause, timeout
|
||||
|
||||
|
||||
def steamvr_activity(rows):
|
||||
"""Read the unique status row; metrics can precede it in --stats output."""
|
||||
if not isinstance(rows, list):
|
||||
return None
|
||||
levels = [row.get('activity_level') for row in rows
|
||||
if isinstance(row, dict) and row.get('operation') == 'status']
|
||||
return levels[0] if len(levels) == 1 and type(levels[0]) is int else None
|
||||
|
||||
|
||||
def diagnostic_source():
|
||||
return (ROOT / 'scripts/framey_openvr.py').read_text() + '''
|
||||
import json
|
||||
@@ -183,9 +192,7 @@ def main(argv=None):
|
||||
if not before['battery'] or min(before['battery']) < 20:
|
||||
raise RuntimeError('Battery unknown or below 20%')
|
||||
if args.wake or args.exercise:
|
||||
stats = before.get('stats')
|
||||
if (not isinstance(stats, list) or not stats or
|
||||
not isinstance(stats[0], dict) or stats[0].get('activity_level') != 3):
|
||||
if steamvr_activity(before.get('stats')) != 3:
|
||||
raise RuntimeError('Frame is awake or SteamVR activity is unknown; active session untouched')
|
||||
if args.wake or args.exercise:
|
||||
# Independent systemd timer restores power even if the controller is
|
||||
|
||||
@@ -36,16 +36,25 @@ def remote(command, stdin=None, timeout=15):
|
||||
|
||||
|
||||
PROBE = r'''
|
||||
import json, pathlib, subprocess
|
||||
import json, math, pathlib, subprocess
|
||||
power=[]
|
||||
for item in pathlib.Path('/sys/class/power_supply').glob('*/capacity'):
|
||||
try:
|
||||
if (item.parent/'type').read_text().strip() == 'Battery':
|
||||
power.append({'name':item.parent.name,'percent':int(item.read_text().strip())})
|
||||
except (OSError,ValueError): pass
|
||||
proximity=[]
|
||||
for item in pathlib.Path('/sys/bus/iio/devices').glob('iio:device*/in_proximity_raw'):
|
||||
try:
|
||||
value=float(item.read_text().strip())
|
||||
if math.isfinite(value) and value >= 0: proximity.append(value)
|
||||
except (OSError,ValueError): pass
|
||||
stats=subprocess.run(['/opt/steamvr/bin/linuxarm64/vrcmd','--stats'],capture_output=True,text=True,timeout=5)
|
||||
try: activity=json.loads(stats.stdout)[0]['activity_level'] if stats.returncode == 0 else None
|
||||
except (ValueError,KeyError,IndexError,TypeError): activity=None
|
||||
try:
|
||||
rows=json.loads(stats.stdout) if stats.returncode == 0 else None
|
||||
levels=[row.get('activity_level') for row in rows if isinstance(row,dict) and row.get('operation')=='status'] if isinstance(rows,list) else []
|
||||
activity=levels[0] if len(levels)==1 and type(levels[0]) is int else None
|
||||
except (ValueError,TypeError): activity=None
|
||||
panels=subprocess.run(['/opt/steamvr/bin/linuxarm64/vrcmd','--overlays'],capture_output=True,text=True,timeout=5)
|
||||
panel=next((line.strip() for line in panels.stdout.splitlines() if 'valve.steam.desktopgame.2001947813' in line),None)
|
||||
base=pathlib.Path.home()/'.local/share/frame-control/decky/plugins'
|
||||
@@ -59,7 +68,7 @@ if base.is_dir() and not base.is_symlink():
|
||||
if isinstance(p.get('storeId'),int): plugins.append({'id':p['storeId'],'name':manifest['name']})
|
||||
except (OSError,ValueError,KeyError): pass
|
||||
service=subprocess.run(['systemctl','--user','is-active','frame-control-headset.service'],capture_output=True,text=True)
|
||||
print(json.dumps({'battery':power,'activity':activity,'panel':panel,
|
||||
print(json.dumps({'battery':power,'proximity':proximity,'activity':activity,'panel':panel,
|
||||
'panelService':service.returncode==0,'plugins':plugins}))
|
||||
'''
|
||||
|
||||
@@ -117,11 +126,15 @@ def main(worn_confirmed=False):
|
||||
raise RuntimeError('Battery capacity unavailable or below 20%; no device session started')
|
||||
if before['activity'] is None or before['activity'] >= 3:
|
||||
raise RuntimeError('SteamVR is in standby; no worn-headset check started')
|
||||
if not before.get('proximity'):
|
||||
raise RuntimeError('Proximity reading unavailable; no worn-headset check started')
|
||||
if not before['panelService'] or not before['plugins']:
|
||||
raise RuntimeError('Install and open the Frame Control panel and a Framey plugin before this check')
|
||||
after_lock = probe()
|
||||
if after_lock['activity'] >= 3 or min(x['percent'] for x in after_lock['battery']) < 20:
|
||||
raise RuntimeError('Activity or battery changed after acquiring the lock')
|
||||
if (not isinstance(after_lock.get('activity'), int) or after_lock['activity'] >= 3 or
|
||||
not after_lock.get('proximity') or not after_lock.get('battery') or
|
||||
min(x['percent'] for x in after_lock['battery']) < 20):
|
||||
raise RuntimeError('Activity, proximity or battery changed after acquiring the lock')
|
||||
state = json.dumps({'nonce':token,'started':time.time()})
|
||||
code = "import pathlib; p=pathlib.Path.home()/'.local/share/frame-control/headset/worn-check.json'; p.write_text(" + repr(state) + ")"
|
||||
remote('python3 -c ' + shlex.quote(code))
|
||||
|
||||
@@ -76,17 +76,22 @@ class DeckyServiceScript(unittest.TestCase):
|
||||
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())
|
||||
service = Path(self.tmp.name) / '.config/systemd/user/frame-control-decky.service'
|
||||
first_unit = service.read_text()
|
||||
self.assertIn('Environment=PATH='+str(Path(self.tmp.name) / '.local/bin')+
|
||||
':/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n', first_unit)
|
||||
|
||||
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')
|
||||
self.assertEqual(service.read_text(), first_unit)
|
||||
|
||||
result = self.apply('update', second, version='v3.3.1')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual((self.base / 'PluginLoader').read_bytes(), second)
|
||||
self.assertIn('Environment=PATH=', service.read_text())
|
||||
result = self.apply('remove')
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse((self.base / 'PluginLoader').exists())
|
||||
|
||||
@@ -15,6 +15,18 @@ spec.loader.exec_module(check)
|
||||
|
||||
|
||||
class Safety(unittest.TestCase):
|
||||
def test_status_row_can_follow_metrics_and_ambiguous_status_fails_closed(self):
|
||||
self.assertEqual(check.steamvr_activity([
|
||||
{'operation':'performance','frame_presents':12},
|
||||
{'operation':'status','activity_level':3}]), 3)
|
||||
self.assertEqual(check.steamvr_activity([
|
||||
{'operation':'performance','frame_presents':12},
|
||||
{'operation':'status','activity_level':1}]), 1)
|
||||
self.assertIsNone(check.steamvr_activity([
|
||||
{'operation':'status','activity_level':3},
|
||||
{'operation':'status','activity_level':1}]))
|
||||
self.assertIsNone(check.steamvr_activity([{'activity_level':3}]))
|
||||
|
||||
def test_overlay_wait_is_bounded_and_stops_when_visible(self):
|
||||
from unittest.mock import Mock
|
||||
remote = Mock()
|
||||
@@ -61,7 +73,7 @@ class Safety(unittest.TestCase):
|
||||
def test_power_restore_after_partial_setter_failure(self):
|
||||
state = {'battery':[99], 'pause':'power.pauseCompositorOnStandby=true',
|
||||
'timeout':'power.turnOffScreensTimeout=17.000000','dashboardVisible':False,
|
||||
'stats':[{'activity_level':3}], 'proximity':[6.25]}
|
||||
'stats':[{'operation':'status','activity_level':3}], 'proximity':[6.25]}
|
||||
with tempfile.TemporaryDirectory() as folder, patch.object(check, 'Remote') as factory:
|
||||
remote = factory.return_value
|
||||
remote.python.return_value = state
|
||||
@@ -78,7 +90,9 @@ class Safety(unittest.TestCase):
|
||||
self.assertIn('rmdir /tmp/frame-test.lock', commands[-1])
|
||||
|
||||
def test_awake_or_unknown_activity_refuses_wake_and_install(self):
|
||||
for stats in ([{'activity_level':1}], [{'activity_level':2}], [], None):
|
||||
for stats in ([{'operation':'status','activity_level':1}],
|
||||
[{'operation':'performance'}, {'operation':'status','activity_level':2}],
|
||||
[], None):
|
||||
state = {'battery':[99], 'pause':'power.pauseCompositorOnStandby=true',
|
||||
'timeout':'power.turnOffScreensTimeout=17.000000',
|
||||
'dashboardVisible':False, 'stats':stats, 'proximity':[0.0]}
|
||||
|
||||
@@ -3,7 +3,9 @@ import sandbox # noqa: F401
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -30,7 +32,7 @@ class WornGate(unittest.TestCase):
|
||||
self.assertIn('trusted pointer events alone do not prove',out.getvalue())
|
||||
|
||||
def test_standby_is_read_only_under_owned_lock_and_releases_it(self):
|
||||
state = {'battery':[{'percent':99}], 'activity':3, 'panelService':True,
|
||||
state = {'battery':[{'percent':99}], 'activity':3, 'proximity':[6.25], 'panelService':True,
|
||||
'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
|
||||
code, report, remote, probe = self.run_gate(state)
|
||||
self.assertEqual(code, 1)
|
||||
@@ -41,7 +43,7 @@ class WornGate(unittest.TestCase):
|
||||
self.assertIn('rmdir /tmp/frame-test.lock', remote.call_args_list[1].args[0])
|
||||
|
||||
def test_low_battery_releases_owned_lock(self):
|
||||
state = {'battery':[{'percent':19}], 'activity':0, 'panelService':True,
|
||||
state = {'battery':[{'percent':19}], 'activity':0, 'proximity':[6.25], 'panelService':True,
|
||||
'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
|
||||
code, report, remote, probe = self.run_gate(state)
|
||||
self.assertEqual(code, 1)
|
||||
@@ -50,7 +52,7 @@ class WornGate(unittest.TestCase):
|
||||
self.assertEqual(remote.call_count, 2)
|
||||
|
||||
def test_foreign_lock_prevents_all_sensor_reads_and_is_never_removed(self):
|
||||
state = {'battery':[{'percent':99}], 'activity':0, 'panelService':True,
|
||||
state = {'battery':[{'percent':99}], 'activity':0, 'proximity':[6.25], 'panelService':True,
|
||||
'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
|
||||
code, report, remote, probe = self.run_gate(state, RuntimeError('File exists'))
|
||||
self.assertEqual(code, 1)
|
||||
@@ -61,3 +63,56 @@ class WornGate(unittest.TestCase):
|
||||
|
||||
def test_screenshot_palette_counts_distinct_colors(self):
|
||||
self.assertEqual(worn.color_count('0 #ff00aa 1 #ff00aa 2 #00ff00'), 2)
|
||||
|
||||
def test_missing_proximity_stops_before_panel_or_input_and_releases_lock(self):
|
||||
state = {'battery':[{'percent':99}], 'activity':0, 'proximity':[],
|
||||
'panelService':True, 'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
|
||||
code, report, remote, probe = self.run_gate(state)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn('Proximity reading unavailable', report)
|
||||
probe.assert_called_once()
|
||||
self.assertEqual(remote.call_count, 2)
|
||||
self.assertIn('rmdir /tmp/frame-test.lock', remote.call_args_list[-1].args[0])
|
||||
|
||||
def test_proximity_disappearing_on_second_read_stops_before_input(self):
|
||||
first = {'battery':[{'percent':99}], 'activity':0, 'proximity':[200.0],
|
||||
'panelService':True, 'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
|
||||
second = {**first, 'proximity':[]}
|
||||
out = io.StringIO()
|
||||
with patch.object(worn, 'probe', side_effect=[first, second]) as probe, \
|
||||
patch.object(worn, 'remote') as remote, contextlib.redirect_stdout(out):
|
||||
code = worn.main(worn_confirmed=True)
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn('Activity, proximity or battery changed', out.getvalue())
|
||||
self.assertEqual(probe.call_count, 2)
|
||||
self.assertEqual(remote.call_count, 2)
|
||||
|
||||
def test_probe_reads_activity_from_status_after_metrics(self):
|
||||
rows = [{'operation':'performance','frame_presents':12},
|
||||
{'operation':'status','activity_level':1}]
|
||||
def run(argv, **_):
|
||||
if argv[-1] == '--stats':
|
||||
return SimpleNamespace(returncode=0, stdout=json.dumps(rows))
|
||||
return SimpleNamespace(returncode=1, stdout='')
|
||||
for current, expected in (
|
||||
(rows, 1),
|
||||
([{'operation':'performance','frame_presents':12}], None),
|
||||
(rows + [{'operation':'status','activity_level':3}], None),
|
||||
):
|
||||
with self.subTest(rows=current):
|
||||
rows = current
|
||||
out = io.StringIO()
|
||||
with patch('subprocess.run', side_effect=run), contextlib.redirect_stdout(out):
|
||||
exec(worn.PROBE, {})
|
||||
result = json.loads(out.getvalue())
|
||||
self.assertEqual(result['activity'], expected)
|
||||
self.assertIsInstance(result['proximity'], list)
|
||||
|
||||
def test_probe_discards_nonfinite_proximity(self):
|
||||
def glob(_path, pattern):
|
||||
return [SimpleNamespace(read_text=lambda: 'nan')] if pattern == 'iio:device*/in_proximity_raw' else []
|
||||
with patch('pathlib.Path.glob', autospec=True, side_effect=glob), \
|
||||
patch('subprocess.run', return_value=SimpleNamespace(returncode=1, stdout='')), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
exec(worn.PROBE, {})
|
||||
self.assertEqual(json.loads(out.getvalue())['proximity'], [])
|
||||
@@ -123,6 +123,12 @@ try:
|
||||
next_binary.write_bytes(data)
|
||||
os.replace(next_binary, binary)
|
||||
binary.chmod(0o700)
|
||||
# The Frame's user-installed tools (including its Tailscale CLI)
|
||||
# live here, while the systemd user manager omits this directory.
|
||||
# Keep standard system paths for plugins that invoke OS tools.
|
||||
plugin_path = ':'.join((str(home / '.local/bin'), '/usr/local/sbin',
|
||||
'/usr/local/bin', '/usr/sbin', '/usr/bin',
|
||||
'/sbin', '/bin'))
|
||||
text = ('[Unit]\nDescription=Decky Loader (Frame Control experimental user install)\n'
|
||||
'After=default.target\n[Service]\nType=simple\nRestart=on-failure\n'
|
||||
'RestartSec=5\nExecStart='+str(binary)+'\n'
|
||||
@@ -130,6 +136,7 @@ try:
|
||||
'Environment=PRIVILEGED_PATH='+str(base)+'\n'
|
||||
'Environment=CHOWN_PLUGIN_PATH=0\nEnvironment=LIVE_RELOAD=0\n'
|
||||
'Environment=USE_CEF_CLOSE_WORKAROUND=0\nEnvironment=KEEP_SYSTEMD_SERVICE=1\n'
|
||||
'Environment=PATH='+plugin_path+'\n'
|
||||
'[Install]\nWantedBy=default.target\n')
|
||||
service.write_text(text)
|
||||
receipt.write_text(json.dumps({'managedBy':'Frame Control','version':version,'sha256':digest})+'\n')
|
||||
|
||||
@@ -3185,12 +3185,13 @@
|
||||
"https://github.com/tailscale-dev/deck-tailscale"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Settings render locally after the SuspensefulImage fallback. Tailscale daemon/state, connection and exit-node changes unverified; bundled localhost QR asset remains unavailable. No network settings were changed.",
|
||||
"reason": "Settings render locally after the SuspensefulImage fallback. On the real Frame, exact official read-only backend status calls returned live state, exit-node and device entries when ~/.local/bin was in PATH; a transient user unit found the CLI with the proposed managed-service PATH. Live Framey Decky RPC, connection/exit-node changes and worn VR remain unverified; the bundled localhost QR asset is unavailable. No network settings were changed.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"fake-Frame",
|
||||
"real Frame read-only backend"
|
||||
],
|
||||
"browser": {
|
||||
"id": 43,
|
||||
|
||||
Reference in new issue
Block a user