mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 01:00:18 +02:00
Devices: the assistant's keep-awake and panel tools reach the chosen headset; a Mac view tunnel opened during a switch is dropped
Integration review findings: the scripts they run ssh'd to whatever 'frame' means in ~/.ssh/config. They now take FRAME_ALIAS and FRAME_SSH_OPTS from the server's route. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
08fbe730c6
commit
ffef4d897a
5 files changed
+39
-6
No files matched your search
@@ -15,11 +15,13 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
FRAME_ALIAS=${FRAME_ALIAS:-frame}
|
FRAME_ALIAS=${FRAME_ALIAS:-frame}
|
||||||
|
# Frame Control passes the headset it has chosen: its address and pinned identity.
|
||||||
|
ssh_opts=(${(Q)${(z)FRAME_SSH_OPTS:-}})
|
||||||
HERE=${0:A:h}
|
HERE=${0:A:h}
|
||||||
cmd=${1:-status}
|
cmd=${1:-status}
|
||||||
case $cmd in on|off|status) ;; *) echo "usage: keep-awake.sh on|off|status" >&2; exit 2 ;; esac
|
case $cmd in on|off|status) ;; *) echo "usage: keep-awake.sh on|off|status" >&2; exit 2 ;; esac
|
||||||
|
|
||||||
ssh -o ConnectTimeout=8 "$FRAME_ALIAS" \
|
ssh "${ssh_opts[@]}" -o ConnectTimeout=8 "$FRAME_ALIAS" \
|
||||||
'mkdir -p ~/.cache/frame-control && cat > ~/.cache/frame-control/frame_steam.py' < "$HERE/../ui/frame_steam.py"
|
'mkdir -p ~/.cache/frame-control && cat > ~/.cache/frame-control/frame_steam.py' < "$HERE/../ui/frame_steam.py"
|
||||||
|
|
||||||
# Runs on the Frame. Verified 2026-09-28 (BUILD_ID 20260925.6191901): the
|
# Runs on the Frame. Verified 2026-09-28 (BUILD_ID 20260925.6191901): the
|
||||||
@@ -27,7 +29,7 @@ ssh -o ConnectTimeout=8 "$FRAME_ALIAS" \
|
|||||||
# written the way Steam's settings page does (steamui module exporting the
|
# written the way Steam's settings page does (steamui module exporting the
|
||||||
# SetSetting wrapper). logind refuses an inhibitor from an SSH session
|
# SetSetting wrapper). logind refuses an inhibitor from an SSH session
|
||||||
# ("Interactive authentication required") but allows one from a user unit.
|
# ("Interactive authentication required") but allows one from a user unit.
|
||||||
ssh "$FRAME_ALIAS" python3 - "$cmd" <<'EOF'
|
ssh "${ssh_opts[@]}" "$FRAME_ALIAS" python3 - "$cmd" <<'EOF'
|
||||||
import json, os, subprocess, sys
|
import json, os, subprocess, sys
|
||||||
sys.path.insert(0, os.path.expanduser("~/.cache/frame-control"))
|
sys.path.insert(0, os.path.expanduser("~/.cache/frame-control"))
|
||||||
from frame_steam import Page
|
from frame_steam import Page
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
FRAME_ALIAS=${FRAME_ALIAS:-frame}
|
FRAME_ALIAS=${FRAME_ALIAS:-frame}
|
||||||
|
# Frame Control passes the headset it has chosen: its address and pinned identity.
|
||||||
|
ssh_opts=(${(Q)${(z)FRAME_SSH_OPTS:-}})
|
||||||
REMMINA_PROFILE="~/.var/app/org.remmina.Remmina/data/remmina/mac-screen-sharing.remmina"
|
REMMINA_PROFILE="~/.var/app/org.remmina.Remmina/data/remmina/mac-screen-sharing.remmina"
|
||||||
id="" name=""
|
id="" name=""
|
||||||
|
|
||||||
@@ -109,4 +111,4 @@ EOF
|
|||||||
)
|
)
|
||||||
b64=$(print -rn -- "$remote" | base64)
|
b64=$(print -rn -- "$remote" | base64)
|
||||||
|
|
||||||
ssh "$FRAME_ALIAS" "bash -c \"\$(echo $b64 | base64 -d)\" panel-on-frame $id ${(j: :)${(@q)cmd}}"
|
ssh "${ssh_opts[@]}" "$FRAME_ALIAS" "bash -c \"\$(echo $b64 | base64 -d)\" panel-on-frame $id ${(j: :)${(@q)cmd}}"
|
||||||
@@ -252,3 +252,23 @@ class ComputerState(unittest.TestCase):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptsFollowTheHeadset(unittest.TestCase):
|
||||||
|
"""keep_awake and panel tools run scripts that ssh on their own: they must reach the
|
||||||
|
headset the server is routed to, not whatever `frame` means in ~/.ssh/config."""
|
||||||
|
|
||||||
|
@unittest.skipUnless(shutil.which('zsh'), 'needs zsh')
|
||||||
|
def test_scripts_get_the_routed_alias_and_options(self):
|
||||||
|
import shlex
|
||||||
|
fake = mock.Mock(FRAME='frame-2', LOCAL=False, HERE=Path(__file__).resolve().parent.parent / 'ui',
|
||||||
|
SSH=['ssh', '-o', 'BatchMode=yes', '-o', 'HostName=192.0.2.2', '-o', 'HostKeyAlias=frame-control-ab'])
|
||||||
|
with mock.patch.object(agent.subprocess, 'run', return_value=mock.Mock(returncode=0, stdout='ok', stderr='')) as run:
|
||||||
|
agent.run_script(fake, 'keep-awake.sh', ['status'])
|
||||||
|
env = run.call_args.kwargs['env']
|
||||||
|
self.assertEqual(env['FRAME_ALIAS'], 'frame-2')
|
||||||
|
self.assertEqual(shlex.split(env['FRAME_SSH_OPTS']), fake.SSH[1:])
|
||||||
|
# and the script turns that back into the same argv
|
||||||
|
out = subprocess.run(['zsh', '-c', 'ssh_opts=(${(Q)${(z)FRAME_SSH_OPTS:-}}); print -l -- $ssh_opts'],
|
||||||
|
env={**os.environ, 'FRAME_SSH_OPTS': env['FRAME_SSH_OPTS']}, capture_output=True, text=True)
|
||||||
|
self.assertEqual(out.stdout.splitlines(), fake.SSH[1:])
|
||||||
+6
-1
@@ -1,7 +1,9 @@
|
|||||||
"""Agent actions and one-use human approvals. No model SDK or network calls here."""
|
"""Agent actions and one-use human approvals. No model SDK or network calls here."""
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import secrets
|
import secrets
|
||||||
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
@@ -129,7 +131,10 @@ def run_script(server, name, args):
|
|||||||
script = server.HERE.parent / 'scripts' / name
|
script = server.HERE.parent / 'scripts' / name
|
||||||
if not script.exists() or not shutil.which('zsh') or server.LOCAL:
|
if not script.exists() or not shutil.which('zsh') or server.LOCAL:
|
||||||
raise ValueError(name + ' requires a computer with zsh and the matching script installed')
|
raise ValueError(name + ' requires a computer with zsh and the matching script installed')
|
||||||
result = subprocess.run(['zsh', str(script), *args], capture_output=True, text=True, timeout=60)
|
# The headset the server is routed to, not whatever `frame` means in ~/.ssh/config.
|
||||||
|
env = {**os.environ, 'FRAME_ALIAS': server.FRAME,
|
||||||
|
'FRAME_SSH_OPTS': shlex.join(server.SSH[1:])}
|
||||||
|
result = subprocess.run(['zsh', str(script), *args], capture_output=True, text=True, timeout=60, env=env)
|
||||||
if result.returncode:
|
if result.returncode:
|
||||||
raise ValueError(result.stderr.strip() or 'Script failed')
|
raise ValueError(result.stderr.strip() or 'Script failed')
|
||||||
return {'message': result.stdout.strip()}
|
return {'message': result.stdout.strip()}
|
||||||
|
|||||||
+6
-2
@@ -262,12 +262,13 @@ class MacView:
|
|||||||
"""Tries the ports on one route; True once the tunnel answers. With self.lock held."""
|
"""Tries the ports on one route; True once the tunnel answers. With self.lock held."""
|
||||||
last = ""
|
last = ""
|
||||||
for port in ports:
|
for port in ports:
|
||||||
|
target = (self.frame, self.host_opts) # retarget() may change these meanwhile
|
||||||
# `via` first: ssh keeps the first value of an option, so USB-C's HostName wins
|
# `via` first: ssh keeps the first value of an option, so USB-C's HostName wins
|
||||||
# while the headset's pinned identity (in host_opts) still checks it.
|
# while the headset's pinned identity (in host_opts) still checks it.
|
||||||
proc = subprocess.Popen([*self.tunnel_ssh, "-o", "ControlPath=none", *via, *self.host_opts,
|
proc = subprocess.Popen([*self.tunnel_ssh, "-o", "ControlPath=none", *via, *target[1],
|
||||||
"-o", "ExitOnForwardFailure=yes",
|
"-o", "ExitOnForwardFailure=yes",
|
||||||
"-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=3", "-N",
|
"-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=3", "-N",
|
||||||
"-R", f"127.0.0.1:{port}:127.0.0.1:{self.port}", self.frame],
|
"-R", f"127.0.0.1:{port}:127.0.0.1:{self.port}", target[0]],
|
||||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE, text=True)
|
stderr=subprocess.PIPE, text=True)
|
||||||
# A taken port makes ssh exit once it's connected; a working
|
# A taken port makes ssh exit once it's connected; a working
|
||||||
@@ -280,6 +281,9 @@ class MacView:
|
|||||||
if self._probe(port):
|
if self._probe(port):
|
||||||
ok = True
|
ok = True
|
||||||
break
|
break
|
||||||
|
if ok and target[0] != self.frame:
|
||||||
|
ok = False # the app switched headset while this one connected: not its tunnel
|
||||||
|
self._last_tunnel_error = "switched headset"
|
||||||
if ok:
|
if ok:
|
||||||
self.tunnel, self.remote_port = proc, port
|
self.tunnel, self.remote_port = proc, port
|
||||||
self.track(proc)
|
self.track(proc)
|
||||||
|
|||||||
Reference in new issue
Block a user