From 0280e7441db026445adf67b7e8ff581331687adb Mon Sep 17 00:00:00 2001 From: DeeJanuz <45082401+DeeJanuz@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:04:49 -0600 Subject: [PATCH] Installers: ask for sudo in the terminal, and cope with SteamVR off - frame_sudo (scripts/_env.sh) replaces three copies of sudo_run. On the Frame it asks in the terminal even when stdin isn't one, which install.sh's steps never had: saying yes to the Bluetooth fixes or hand tracking stopped the install with "no terminal for sudo". From a PC it asks through ssh -t when .env has no password. - start_with_steamvr: the pointer, power, and gaze services restart when SteamVR runs (a re-install runs the new code) and are left to start with it when it doesn't, instead of failing the install. Co-Authored-By: Claude Opus 5.5 --- gaze/run.sh | 4 ++-- gaze/tracker/install.sh | 23 ++++---------------- hands/run.sh | 18 ++++------------ pointer/helper/run.sh | 6 ++---- power/run.sh | 6 ++---- scripts/_env.sh | 44 ++++++++++++++++++++++++++++++++++++++ setup/README.md | 2 +- setup/bluetooth/install.sh | 17 +++------------ 8 files changed, 62 insertions(+), 58 deletions(-) diff --git a/gaze/run.sh b/gaze/run.sh index c30920b..5fe8305 100755 --- a/gaze/run.sh +++ b/gaze/run.sh @@ -12,8 +12,8 @@ case ${1:-status} in "$root/gaze/build.sh" fill_template "$root/gaze/$unit" | on_frame "mkdir -p ~/.config/systemd/user && cat > ~/.config/systemd/user/$unit" on_frame "chmod +x gaze/ft-gazed gaze/ft-gazectl" - "$frame" --host "set -e; systemctl --user daemon-reload; systemctl --user enable --now $unit -sleep 2; echo \"$unit: \$(systemctl --user is-active $unit)\"; journalctl --user -u $unit --no-pager -o cat -n 5" ;; + "$frame" --host "set -e; systemctl --user daemon-reload; systemctl --user enable $unit +$(start_with_steamvr $unit)" ;; uninstall) "$frame" --host "systemctl --user disable --now $unit 2>/dev/null; rm -f ~/.config/systemd/user/$unit; systemctl --user daemon-reload; echo removed" ;; start|stop|restart) "$frame" --host "systemctl --user $1 $unit; systemctl --user is-active $unit" ;; status) "$frame" --host "systemctl --user is-active $unit" || true; on_frame "gaze/ft-gazectl status" || true ;; diff --git a/gaze/tracker/install.sh b/gaze/tracker/install.sh index cfb79f1..0cdf3c1 100755 --- a/gaze/tracker/install.sh +++ b/gaze/tracker/install.sh @@ -4,10 +4,9 @@ # SteamVR's eyetracking process into /dev/shm/frametop-eyes-cams for ft-eyes, and only while # ft-eyes wants them. The gaze service (gaze/ft-gazed) runs ft-eyes itself, when Eye tracker # is Own tracker or the gaze probe uses it. -# Needs host sudo, for the binary (/etc/frametop/ft-eyegrab, root's) and the unit. On the -# Frame, sudo asks for the password in the terminal, or runs SUDO_ASKPASS when that's set. -# From a PC (or with no terminal), the password comes from steamos_root_pwd in the repo's .env -# and is sent to sudo -S on stdin, never on a command line. +# Needs host sudo, for the binary (/etc/frametop/ft-eyegrab, root's) and the unit: it asks for +# the password in the terminal, on the Frame or from a PC, or runs SUDO_ASKPASS when that's set +# (frame_sudo in scripts/_env.sh, which also takes it from the repo's .env). # Usage: gaze/tracker/install.sh [install|uninstall|status|log [lines]] set -euo pipefail @@ -16,21 +15,7 @@ root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) src=$FRAME_REPO/gaze/tracker unit=frametop-eyegrab.service -sudo_run() { - if [ "$FRAME_LOCAL" = 1 ] && [ -n "${SUDO_ASKPASS:-}" ]; then - sudo -A bash -c "$1" # SUDO_ASKPASS supplies the password - return - fi - if [ "$FRAME_LOCAL" = 1 ] && [ -t 0 ]; then - sudo bash -c "$1" # asks for the password here - return - fi - local pw - pw=$(sed -n 's/^steamos_root_pwd=//p' "$root/.env" 2>/dev/null) - pw=${pw#[\"\']}; pw=${pw%[\"\']} # .env values may be quoted - [ -n "$pw" ] || { echo "no terminal for sudo, and steamos_root_pwd is missing from $root/.env" >&2; exit 1; } - printf '%s\n' "$pw" | on_frame "sudo -S -p '' bash -c $(printf %q "$1")" -} +sudo_run() { frame_sudo "$1"; } case ${1:-install} in install) diff --git a/hands/run.sh b/hands/run.sh index 9b0cbb2..e5f89f1 100755 --- a/hands/run.sh +++ b/hands/run.sh @@ -5,9 +5,9 @@ # Usage: hands/run.sh install|uninstall # hands/run.sh caps # give ft-camd its capabilities again (a rebuild clears them) # hands/run.sh start|stop|restart|status|log [lines] -# install and caps need the password (sudo setcap, once per build of ft-camd). On the Frame, -# sudo asks in the terminal. From a PC (or with no terminal), the password comes from -# steamos_root_pwd in the repo's .env and is sent to sudo -S on stdin, never on a command line. +# install and caps need the password (sudo setcap, once per build of ft-camd): it's asked in +# the terminal, on the Frame or from a PC (frame_sudo in scripts/_env.sh, which also takes it +# from the repo's .env). set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) . "$root/scripts/_env.sh" @@ -17,17 +17,7 @@ units="frametop-camd.service frametop-hands.service" # format files. ft-camd drops them all once it has set up. caps=cap_sys_ptrace,cap_perfmon,cap_dac_read_search+ep -sudo_run() { - if [ "$FRAME_LOCAL" = 1 ] && [ -t 0 ]; then - sudo bash -c "$1" # asks for the password here - return - fi - local pw - pw=$(sed -n 's/^steamos_root_pwd=//p' "$root/.env" 2>/dev/null) - pw=${pw#[\"\']}; pw=${pw%[\"\']} # .env values may be quoted - [ -n "$pw" ] || { echo "no terminal for sudo, and steamos_root_pwd is missing from $root/.env" >&2; exit 1; } - printf '%s\n' "$pw" | on_frame "sudo -S -p '' bash -c $(printf %q "$1")" -} +sudo_run() { frame_sudo "$1"; } set_caps() { # only when missing: a rebuild clears them, a reinstall doesn't local bin diff --git a/pointer/helper/run.sh b/pointer/helper/run.sh index 60533c1..ecbbcb3 100755 --- a/pointer/helper/run.sh +++ b/pointer/helper/run.sh @@ -14,10 +14,8 @@ case ${1:-status} in "$root/scripts/sync.sh" >/dev/null fill_template "$root/pointer/helper/$unit" | on_frame "mkdir -p ~/.config/systemd/user && cat > ~/.config/systemd/user/$unit" "$frame" --host "set -e; pkill -x ft-pointer || true -systemctl --user daemon-reload; systemctl --user enable --now $unit -# It comes up once SteamVR runs (it starts with it); distrobox enter takes a few seconds. -for i in \$(seq 20); do systemctl --user is-active --quiet $unit && break; sleep 1; done -echo \"$unit: \$(systemctl --user is-active $unit)\"; journalctl --user -u $unit --no-pager -o cat -n 3" ;; +systemctl --user daemon-reload; systemctl --user enable $unit +$(start_with_steamvr $unit)" ;; uninstall) "$frame" --host "systemctl --user disable --now $unit 2>/dev/null; rm -f ~/.config/systemd/user/$unit; systemctl --user daemon-reload; echo removed" ;; start|stop|restart) if installed; then "$frame" --host "systemctl --user $1 $unit; systemctl --user is-active $unit"; exit; fi ;;& log) if installed; then "$frame" --host "journalctl --user -u $unit --no-pager -o cat -n ${2:-30}"; exit; fi ;;& diff --git a/power/run.sh b/power/run.sh index ce70179..3a2d372 100755 --- a/power/run.sh +++ b/power/run.sh @@ -13,10 +13,8 @@ case ${1:-status} in "$root/scripts/sync.sh" >/dev/null fill_template "$root/power/$unit" | on_frame "mkdir -p ~/.config/systemd/user && cat > ~/.config/systemd/user/$unit" "$frame" --host "set -e; pkill -x ft-powerd || true -systemctl --user daemon-reload; systemctl --user enable --now $unit -# It comes up once SteamVR runs (it starts with it); distrobox enter takes a few seconds. -for i in \$(seq 20); do systemctl --user is-active --quiet $unit && break; sleep 1; done -echo \"$unit: \$(systemctl --user is-active $unit)\"; journalctl --user -u $unit --no-pager -o cat -n 3" ;; +systemctl --user daemon-reload; systemctl --user enable $unit +$(start_with_steamvr $unit)" ;; uninstall) "$frame" --host "systemctl --user disable --now $unit 2>/dev/null; rm -f ~/.config/systemd/user/$unit; systemctl --user daemon-reload; echo removed" ;; start|stop|restart) "$frame" --host "systemctl --user $1 $unit; systemctl --user is-active $unit" ;; status) "$frame" --host "systemctl --user is-active $unit; python3 -c 'import socket; s=socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM); s.bind(\"\"); s.settimeout(1); s.sendto(b\"status\", \"\\0ft_powerd\"); print(s.recv(256).decode())' 2>/dev/null || echo 'ft-powerd not answering'" ;; diff --git a/scripts/_env.sh b/scripts/_env.sh index c64848f..0b1fff1 100755 --- a/scripts/_env.sh +++ b/scripts/_env.sh @@ -58,3 +58,47 @@ on_frame_script() { fill_template() { sed "s|@REPO@|$FRAME_REPO|g" "$1" } + +# frame_sudo '': run a shell command as root on the Frame host. sudo asks for the +# password in this terminal: on the Frame directly (it reads the terminal itself, so this +# works when stdin isn't one, as in install.sh's steps), or from a PC through ssh -t. +# SUDO_ASKPASS on the Frame, or steamos_root_pwd in the repo's .env (sent to sudo -S on +# stdin, never on a command line) answer it with no terminal; from a PC, .env comes first. +frame_sudo() { + local tty=0 pw + { : /dev/null && tty=1 + if [ "$FRAME_LOCAL" = 1 ] && [ -n "${SUDO_ASKPASS:-}" ]; then + sudo -A bash -c "$1" + return + fi + if [ "$FRAME_LOCAL" = 1 ] && [ "$tty" = 1 ]; then + sudo bash -c "$1" + return + fi + pw=$(sed -n 's/^steamos_root_pwd=//p' "$REPO_ROOT/.env" 2>/dev/null) + pw=${pw#[\"\']}; pw=${pw%[\"\']} # .env values may be quoted + if [ -n "$pw" ]; then + printf '%s\n' "$pw" | on_frame "sudo -S -p '' bash -c $(printf %q "$1")" + elif [ "$FRAME_LOCAL" = 0 ] && [ "$tty" = 1 ]; then + ssh -tt -o BatchMode=yes "$FRAME_HOST" "cd $(printf %q "$FRAME_REPO") && sudo bash -c $(printf %q "$1")" &2 + return 1 + fi +} + +# start_with_steamvr UNIT: a host command for an installer. Units that need SteamVR +# (Requisite=steamvr.service) can't start without it, so with SteamVR off (an install over +# SSH, the headset asleep) they're left to start with it. With SteamVR on, the unit restarts, +# so a re-install runs the new code, and the command waits for it and shows its last lines. +start_with_steamvr() { + local unit=$1 + printf '%s' "if systemctl --user is-active --quiet steamvr.service; then +systemctl --user restart $unit +# distrobox enter takes a few seconds. +for i in \$(seq 20); do systemctl --user is-active --quiet $unit && break; sleep 1; done +echo \"$unit: \$(systemctl --user is-active $unit)\"; journalctl --user -u $unit --no-pager -o cat -n 3 +else +echo '$unit: enabled; SteamVR is off, so it starts with SteamVR' +fi" +} diff --git a/setup/README.md b/setup/README.md index e45fd9a..7629aa4 100644 --- a/setup/README.md +++ b/setup/README.md @@ -40,7 +40,7 @@ The service runs after Bluetooth has started and never makes Bluetooth wait for The install writes to `/etc`, so it needs `sudo` and the `steamos` user's password. - On the headset, `sudo` asks for the password in the terminal. If you've never set one, run `passwd` first. -- From a PC over SSH, there's no terminal on the Frame to ask in, so put the password in a `.env` file at the repo root: +- From a PC over SSH, the scripts ask for it in your terminal (through `ssh -t`). To skip the question, or with no terminal, put the password in a `.env` file at the repo root: ``` steamos_root_pwd="your-password" diff --git a/setup/bluetooth/install.sh b/setup/bluetooth/install.sh index c8ae674..8353895 100755 --- a/setup/bluetooth/install.sh +++ b/setup/bluetooth/install.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash # Install (or remove) the persistent Bluetooth LE workarounds on the Frame. -# Needs host sudo. On the Frame, sudo asks for the password in the terminal. -# From a PC (or with no terminal), the password comes from steamos_root_pwd in the -# repo's .env and is sent to sudo -S on stdin, never on a command line. +# Needs host sudo: it asks for the password in the terminal, on the Frame or from a PC +# (frame_sudo in scripts/_env.sh, which also takes it from the repo's .env). # Usage: setup/bluetooth/install.sh [install|uninstall|run] # run re-apply now without restarting bluetooth (after pairing a new device) set -euo pipefail @@ -11,17 +10,7 @@ root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) . "$root/scripts/_env.sh" src=$FRAME_REPO/setup/bluetooth -sudo_run() { - if [ "$FRAME_LOCAL" = 1 ] && [ -t 0 ]; then - sudo bash -c "$1" # asks for the password here - return - fi - local pw - pw=$(sed -n 's/^steamos_root_pwd=//p' "$root/.env" 2>/dev/null) - pw=${pw#[\"\']}; pw=${pw%[\"\']} # .env values may be quoted - [ -n "$pw" ] || { echo "no terminal for sudo, and steamos_root_pwd is missing from $root/.env" >&2; exit 1; } - printf '%s\n' "$pw" | on_frame "sudo -S -p '' bash -c $(printf %q "$1")" -} +sudo_run() { frame_sudo "$1"; } case ${1:-install} in install)