mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 08:00:32 +02:00
Add Steam screenshots, Tailscale remote access, VR-video fixes
- Screenshots: list the Frame's Steam screenshots, open them in the viewer, and save new ones to ~/Pictures/SteamFrame. Ids are validated before any shell, and copies land atomically. - Tailscale: scripts/tailscale-on-frame.sh installs a userspace tailscaled as a lingering systemd --user service with no sudo, SHA-256 checked, safe to re-run, with --uninstall. docs/tailscale.md covers setup and warns that in userspace mode every Frame port, including loopback-only DevTools and ADB, is reachable from the tailnet. - push-vr-video.sh: filenames starting with "-" are safe, symlinks are followed, and a real Videos\VR directory triggers a warning. - Tests cover the screenshot routes (19 total). Docs keep placeholder addresses for the headset and tailnet. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
99653151c4
commit
eabf4cd1f9
10 files changed
+427
-54
No files matched your search
@@ -10,6 +10,11 @@
|
||||
# ~/.config/systemd/user/tailscaled.service
|
||||
# `tailscaled --tun=userspace-networking` needs no /dev/net/tun or root.
|
||||
#
|
||||
# Exposure: in userspace mode tailscaled forwards inbound tailnet connections
|
||||
# to the Frame's loopback, so EVERY port is reachable from the tailnet,
|
||||
# including localhost-only ones (Steam's DevTools on 8080, SteamVR, ADB).
|
||||
# `tailscale set --shields-up` blocks all inbound (SSH too). See docs/tailscale.md.
|
||||
#
|
||||
# Usage: scripts/tailscale-on-frame.sh [--version X.Y.Z] [--hostname NAME]
|
||||
# scripts/tailscale-on-frame.sh --uninstall
|
||||
# The first run prints a login URL (and opens it on the Mac) to add the Frame
|
||||
@@ -20,10 +25,10 @@ FRAME=${FRAME_ALIAS:-frame}
|
||||
version="" hostname="frame" uninstall=0
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--version) version=$2; shift ;;
|
||||
--hostname) hostname=$2; shift ;;
|
||||
--version) version=${2:?--version needs a value}; shift ;;
|
||||
--hostname) hostname=${2:?--hostname needs a value}; shift ;;
|
||||
--uninstall) uninstall=1 ;;
|
||||
-h|--help) sed -n '2,17p' "$0"; exit 0 ;;
|
||||
-h|--help) sed -n "2,21p" "$0"; exit 0 ;;
|
||||
*) print -u2 "unknown argument: $1"; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
@@ -33,13 +38,21 @@ done
|
||||
if (( uninstall )); then
|
||||
ssh "$FRAME" 'set -e
|
||||
systemctl --user disable --now tailscaled.service 2>/dev/null || true
|
||||
rm -f ~/.config/systemd/user/tailscaled.service ~/.local/bin/tailscale ~/.local/bin/tailscaled
|
||||
rm -f ~/.config/systemd/user/tailscaled.service ~/.local/bin/tailscale
|
||||
systemctl --user daemon-reload
|
||||
echo "Removed the service and wrappers. Binaries and node state are still in ~/.local/share/tailscale;"
|
||||
echo "delete that folder and remove the machine in the Tailscale admin console to finish."'
|
||||
echo "Removed the service and the CLI wrapper. Binaries and node state are still in"
|
||||
echo "~/.local/share/tailscale; delete that folder and remove the machine in the"
|
||||
echo "Tailscale admin console to finish. Linger stays on (loginctl disable-linger to undo)."'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# BackendState of the Frame's tailscaled (Running, NeedsLogin, Stopped, …), or
|
||||
# Unreachable when the probe itself fails (SSH down, daemon restarting).
|
||||
ts_state() {
|
||||
ssh -o ConnectTimeout=10 "$FRAME" '~/.local/bin/tailscale status --json 2>/dev/null |
|
||||
python3 -c "import json,sys; print(json.load(sys.stdin)[\"BackendState\"])"' 2>/dev/null || print Unreachable
|
||||
}
|
||||
|
||||
if [[ -z $version ]]; then
|
||||
version=$(curl -fsS "https://pkgs.tailscale.com/stable/?mode=json" |
|
||||
python3 -c 'import json,sys; print(json.load(sys.stdin)["TarballsVersion"])')
|
||||
@@ -47,11 +60,13 @@ fi
|
||||
[[ $version =~ '^[0-9]+\.[0-9]+\.[0-9]+$' ]] || { print -u2 "bad version: $version"; exit 2; }
|
||||
print "==> Installing Tailscale $version on $FRAME (userspace networking)"
|
||||
|
||||
ssh "$FRAME" "VERSION=$version HOSTNAME_TS=$hostname sh -s" <<'REMOTE'
|
||||
remote_out=$(ssh "$FRAME" "VERSION=$version sh -s" <<'REMOTE'
|
||||
set -eu
|
||||
base="$HOME/.local/share/tailscale"
|
||||
dir="$base/$VERSION"
|
||||
tgz="tailscale_${VERSION}_arm64.tgz"
|
||||
unit="$HOME/.config/systemd/user/tailscaled.service"
|
||||
sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/tailscale/tailscaled.sock"
|
||||
mkdir -p "$base/state" "$HOME/.local/bin" "$HOME/.config/systemd/user"
|
||||
|
||||
if [ ! -x "$dir/tailscaled" ]; then
|
||||
@@ -59,12 +74,15 @@ if [ ! -x "$dir/tailscaled" ]; then
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
curl -fsSL -o "$tmp/$tgz" "https://pkgs.tailscale.com/stable/$tgz"
|
||||
want=$(curl -fsSL "https://pkgs.tailscale.com/stable/$tgz.sha256" | cut -d' ' -f1)
|
||||
[ -n "$want" ] || { echo "couldn't fetch $tgz.sha256" >&2; exit 1; }
|
||||
got=$(sha256sum "$tmp/$tgz" | cut -d' ' -f1)
|
||||
[ "$want" = "$got" ] || { echo "checksum mismatch for $tgz" >&2; exit 1; }
|
||||
tar -xzf "$tmp/$tgz" -C "$tmp"
|
||||
mkdir -p "$dir"
|
||||
mv "$tmp/tailscale_${VERSION}_arm64/tailscale" "$tmp/tailscale_${VERSION}_arm64/tailscaled" "$dir/"
|
||||
fi
|
||||
# Neither exists on a first install; don't let that trip set -e.
|
||||
before=$({ readlink "$base/current"; cat "$unit"; } 2>/dev/null || true)
|
||||
ln -sfn "$dir" "$base/current"
|
||||
|
||||
# The CLI looks for the daemon at /var/run/tailscale by default; point it at ours.
|
||||
@@ -74,7 +92,7 @@ exec "$HOME/.local/share/tailscale/current/tailscale" --socket="${XDG_RUNTIME_DI
|
||||
EOF
|
||||
chmod +x "$HOME/.local/bin/tailscale"
|
||||
|
||||
cat > "$HOME/.config/systemd/user/tailscaled.service" <<'EOF'
|
||||
cat > "$unit" <<'EOF'
|
||||
[Unit]
|
||||
Description=Tailscale (userspace networking, no root)
|
||||
After=network-online.target
|
||||
@@ -88,39 +106,74 @@ RestartSec=5
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
# Linger starts user services at boot, before anyone logs in; polkit allows it without sudo.
|
||||
loginctl enable-linger 2>/dev/null || echo "note: couldn't enable linger; tailscaled starts when the session does" >&2
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable tailscaled.service >/dev/null 2>&1
|
||||
systemctl --user restart tailscaled.service
|
||||
after=$(readlink "$base/current"; cat "$unit")
|
||||
restart=0
|
||||
if systemctl --user is-active --quiet tailscaled.service; then
|
||||
[ "$before" = "$after" ] || restart=1
|
||||
else
|
||||
systemctl --user start tailscaled.service
|
||||
fi
|
||||
for i in $(seq 1 50); do
|
||||
[ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/tailscale/tailscaled.sock" ] && break
|
||||
[ -S "$sock" ] && break
|
||||
sleep 0.2
|
||||
done
|
||||
"$HOME/.local/bin/tailscale" version | head -n 1
|
||||
[ -S "$sock" ] || { echo "tailscaled didn't open $sock; see: journalctl --user -u tailscaled" >&2; exit 1; }
|
||||
v=$("$HOME/.local/bin/tailscale" version)
|
||||
printf 'Tailscale %s\n' "$(printf '%s\n' "$v" | head -n 1)"
|
||||
if [ "$restart" = 1 ]; then
|
||||
# This SSH session may itself run over Tailscale, so restart detached, after
|
||||
# it has ended; the Mac waits and reconnects.
|
||||
systemd-run --user --quiet --on-active=3 --timer-property=AccuracySec=100ms --unit=tailscaled-restart --collect \
|
||||
systemctl --user restart tailscaled.service >/dev/null
|
||||
echo "RESTART_SCHEDULED"
|
||||
fi
|
||||
REMOTE
|
||||
)
|
||||
print -r -- "${remote_out//RESTART_SCHEDULED/Restarting tailscaled for the new version or unit…}"
|
||||
|
||||
# `up` blocks until the login is approved, so run it in the background on the
|
||||
# Frame and fetch the URL from its log.
|
||||
state=$(ssh "$FRAME" '~/.local/bin/tailscale status --json 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)[\"BackendState\"])" 2>/dev/null || echo Unknown')
|
||||
if [[ $state != Running ]]; then
|
||||
ssh "$FRAME" "nohup ~/.local/bin/tailscale up --hostname=$hostname --timeout=10m > /tmp/tailscale-up.log 2>&1 &"
|
||||
url=""
|
||||
for i in {1..40}; do
|
||||
url=$(ssh "$FRAME" 'grep -Eo "https://login\.tailscale\.com/[A-Za-z0-9/_-]+" /tmp/tailscale-up.log | head -n 1' || true)
|
||||
[[ -n $url ]] && break
|
||||
sleep 0.5
|
||||
done
|
||||
if [[ -n $url ]]; then
|
||||
# Wait out a scheduled restart, then read a definite state.
|
||||
[[ $remote_out == *RESTART_SCHEDULED* ]] && sleep 6
|
||||
state=""
|
||||
for i in {1..30}; do
|
||||
state=$(ts_state)
|
||||
[[ $state == (Running|NeedsLogin|NeedsMachineAuth|Stopped|NoState) ]] && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
case $state in
|
||||
Running) ;;
|
||||
NeedsLogin|Stopped|NoState)
|
||||
# `up` blocks until the login is approved, so run it as its own transient
|
||||
# unit (it outlives this SSH session) and fetch the URL from its log.
|
||||
ssh "$FRAME" "rm -f /tmp/tailscale-up.log; systemd-run --user --quiet --collect --unit=tailscale-up-\$\$ \
|
||||
sh -c '~/.local/bin/tailscale up --hostname=$hostname --timeout=10m > /tmp/tailscale-up.log 2>&1' >/dev/null"
|
||||
url=""
|
||||
for i in {1..40}; do
|
||||
url=$(ssh "$FRAME" 'grep -Eo "https://login\.tailscale\.com/[A-Za-z0-9/_-]+" /tmp/tailscale-up.log 2>/dev/null | head -n 1' || true)
|
||||
[[ -n $url ]] && break
|
||||
sleep 0.5
|
||||
done
|
||||
[[ -n $url ]] || { print -u2 "No login URL after 20 s; see /tmp/tailscale-up.log on the Frame."; exit 1; }
|
||||
print "==> Approve the Frame in your tailnet: $url"
|
||||
open "$url" 2>/dev/null || true
|
||||
print " Waiting for approval (up to 10 minutes)…"
|
||||
for i in {1..300}; do
|
||||
state=$(ssh "$FRAME" '~/.local/bin/tailscale status --json 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)[\"BackendState\"])"' || true)
|
||||
state=$(ts_state)
|
||||
[[ $state == Running ]] && break
|
||||
sleep 2
|
||||
done
|
||||
else
|
||||
print -u2 "No login URL yet; see /tmp/tailscale-up.log on the Frame."
|
||||
fi
|
||||
fi
|
||||
[[ $state == Running ]] || { print -u2 "Not approved yet (state: $state). Re-run to get a new URL."; exit 1; }
|
||||
;;
|
||||
NeedsMachineAuth) print -u2 "Logged in; approve the device in the Tailscale admin console, then re-run."; exit 1 ;;
|
||||
*) print -u2 "Couldn't read tailscaled's state (last: $state). Check: ssh $FRAME 'journalctl --user -u tailscaled'"; exit 1 ;;
|
||||
esac
|
||||
|
||||
ssh "$FRAME" '~/.local/bin/tailscale status --self --peers=false; printf "Tailscale IP: "; ~/.local/bin/tailscale ip -4'
|
||||
name=$(ssh "$FRAME" '~/.local/bin/tailscale status --json' | python3 -c 'import json,sys; print(json.load(sys.stdin)["Self"]["DNSName"].rstrip("."))')
|
||||
print "==> To use the Frame from anywhere, point the alias at Tailscale:"
|
||||
print " ssh-keyscan -t ed25519 $name >> ~/.ssh/known_hosts # after checking it matches"
|
||||
print " scripts/connect.sh $name"
|
||||
Reference in new issue
Block a user