mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 00:00:07 +02:00
The desktop has its own XDG_CONFIG_HOME (~/.config/frametop), and SteamVR's tools and OpenVR read their path registry from there. So from a terminal in the desktop: - pointer/driver/install.sh (install.sh step 4, every reinstall or update from Konsole) wrote a new registry, ~/.config/frametop/openvr/openvrpaths.vrpath, with only our driver and "runtime": null, and never registered the driver with SteamVR. That stray file then hid SteamVR from every OpenVR program started in the desktop. - scripts/update-check.py (and so doctor.sh and report.sh) reported OpenVR "can't connect to SteamVR as a background app" (VRInitError_Init_PathRegistryNotFound, or InstallationNotFound with the stray file) and "vrcmd --overlays lists no overlays". A user's report of 2026-10-05 showed both, with SteamVR and Frametop's services fine. - ft-layout's vrcmd calls (the gamescope backend) found no overlays. They now run with XDG_CONFIG_HOME=~/.config: the driver installer (install, uninstall, probe, aimhere), update-check.py (for all its checks: nothing there reads the desktop's own config), report.sh's vrpathreg show, and ft-layout's vrcmd. report.sh doesn't set it for the whole report, since session/fix-panels.py --check reads the desktop's Plasma config through it. The driver installer also removes the stray registry (only vrpathreg's, with no runtime), and update-check.py warns about one. And vrpathreg runs `xdg-open vrmonitor://driverinstalled` to tell SteamVR's desktop monitor, which the Frame doesn't have: in the desktop that showed "could not read file vrmonitor://driverinstalled" on every install. A stand-in xdg-open on its PATH takes it, so install.sh no longer filters the step's output. Tested in a fake HOME with a copy of SteamVR's registry, a stray one, and the desktop's XDG_CONFIG_HOME: the new installer removes the stray file, registers the driver in the copy, and never reaches xdg-open; the old one wrote the stray file, left the copy alone, and ran xdg-open. update-check.py from that environment: OpenVR and vrcmd ok (the old one fails both); its stray-registry warning fires on a seeded file. ft-layout's vrcmd: 119 overlays (was 0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
523 lines
25 KiB
Python
Executable File
523 lines
25 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Check what Frametop needs from SteamOS, for after a SteamOS update.
|
|
|
|
On the Frame, a SteamOS update replaces SteamVR, KWin, gamescope, and the kernel along with
|
|
the rest of the OS image. Frametop lives in the home folder and survives it, but it uses
|
|
SteamVR and KWin interfaces that an update can change or drop, some of them undocumented.
|
|
This checks each one, and compares the versions with the last ones recorded as working.
|
|
|
|
scripts/update-check.py # check (scripts/doctor.sh runs this)
|
|
scripts/update-check.py --mark-good # check, then record these versions as working
|
|
|
|
From a PC: ssh frame python3 - [--mark-good] < scripts/update-check.py
|
|
|
|
It only reads. It connects to SteamVR as a background app (which never starts SteamVR),
|
|
runs `vrcmd --overlays` as the pointer helper does, maps the eye tracker's shared memory
|
|
read-only, and follows a controller on vrserver's web socket for a moment. It never starts,
|
|
stops, or restarts anything.
|
|
"""
|
|
import base64
|
|
import json
|
|
import math
|
|
import mmap
|
|
import os
|
|
import platform
|
|
import re
|
|
import socket
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
HOME = os.path.expanduser("~")
|
|
UID = os.getuid()
|
|
KNOWN_GOOD = os.path.join(HOME, ".local/state/frametop/known-good.json")
|
|
STEAMVR_BIN = "/opt/steamvr/bin/linuxarm64"
|
|
LAUNCHER = os.path.join(HOME, ".local/share/applications/deckard-nested-desktop.desktop")
|
|
VRPATHS = os.path.join(HOME, ".config/openvr/openvrpaths.vrpath")
|
|
# The Frametop desktop has its own XDG_CONFIG_HOME (session/frametop-session.sh). An install from a
|
|
# terminal there, before pointer/driver/install.sh set SteamVR's, left vrpathreg's registry here.
|
|
STRAY_VRPATHS = os.path.join(HOME, ".config/frametop/openvr/openvrpaths.vrpath")
|
|
BACKLIGHT = "/sys/class/backlight/ae94000.dsi.0/brightness" # as in power/ft-powerd.cpp
|
|
EYE_MMAP = "/dev/shm/eye-server.mmap"
|
|
VRSERVER = "http://127.0.0.1:27062" # its web socket: input/vrws.py
|
|
PAUSE_STATE = f"/run/user/{os.getuid()}/frametop-pause.json" # input/game_pause.py
|
|
HOST_GLIBC = (2, 39) # the newest the pointer driver may need (pointer/driver/build.sh)
|
|
|
|
# Packages in the OS image that Frametop depends on, and what to try by hand when one changes.
|
|
PACKAGES = {
|
|
"deckard-steamvr-rel": "the 3D mouse on the dashboard, on SteamVR Settings, and on a SteamVR "
|
|
"window's grab bar; picking up a controller; mapped controller buttons; gaze",
|
|
"kwin": "clicks near the far edge of a screen whose scale isn't 1; every screen comes back "
|
|
"after a desktop restart; floating a window; no blur behind the taskbar's menus",
|
|
"plasma-workspace": "the taskbar and panels after a desktop restart; no DiscoverNotifier or "
|
|
"ibus-daemon inside the desktop",
|
|
"at-spi2-core": "an AT-SPI-aware app appears on the nested desktop's accessibility bus; "
|
|
"the registry stops and comes back after a desktop restart",
|
|
"gamescope": "the headset's volume buttons with nothing focused; typing goes where you last clicked",
|
|
"bluez": "a Bluetooth mouse reconnecting after it sleeps",
|
|
}
|
|
KERNEL_HINT = "display power (ft-powerd) and hand tracking"
|
|
|
|
# Frametop's user services, and the control socket each binds once it's up.
|
|
UNITS = {
|
|
"frametop-input-relay": "@frametop_relay",
|
|
"frametop-pointer": "@ft_pointer_helper",
|
|
"frametop-gaze": "@ft_gazed",
|
|
"frametop-power": "@ft_powerd",
|
|
"frametop-camd": None,
|
|
"frametop-hands": None,
|
|
}
|
|
|
|
# eye-server.mmap offsets, the same as gaze/ft-gaze.cpp's (packed, little-endian).
|
|
EYE_COUNTER, EYE_TIME, EYE_OPEN, EYE_NEED = 0x38, 0x157, 0x1CB, 0x1D3
|
|
EYE_VECTORS = (0x15F, 0x16B, 0x19B, 0x1A7) # set 1 left, right; set 2 left, right
|
|
|
|
# Runs in a child process, so a SteamVR that hangs can't hang the check.
|
|
OPENVR_PROBE = r"""
|
|
import ctypes, sys
|
|
lib = ctypes.CDLL(sys.argv[1])
|
|
lib.VR_InitInternal2.restype = ctypes.c_uint32
|
|
lib.VR_InitInternal2.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int, ctypes.c_char_p]
|
|
lib.VR_GetVRInitErrorAsSymbol.restype = ctypes.c_char_p
|
|
lib.VR_IsInterfaceVersionValid.restype = ctypes.c_bool
|
|
lib.VR_IsInterfaceVersionValid.argtypes = [ctypes.c_char_p]
|
|
err = ctypes.c_int(0)
|
|
lib.VR_InitInternal2(ctypes.byref(err), 3, None) # VRApplication_Background
|
|
if err.value:
|
|
print("init", lib.VR_GetVRInitErrorAsSymbol(err.value).decode())
|
|
sys.exit(1)
|
|
for name in sys.argv[2:]:
|
|
print(name, int(lib.VR_IsInterfaceVersionValid(name.encode())))
|
|
lib.VR_ShutdownInternal()
|
|
"""
|
|
|
|
failed = False
|
|
|
|
|
|
def report(state, label, detail=""):
|
|
global failed
|
|
failed = failed or state == "FAIL"
|
|
print(f"{state:<6}{label}{': ' + detail if detail else ''}", flush=True)
|
|
|
|
|
|
def run(*cmd, timeout=15, env=None):
|
|
try:
|
|
p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, env=env)
|
|
return p.returncode, p.stdout
|
|
except (OSError, subprocess.TimeoutExpired) as e:
|
|
return -1, str(e)
|
|
|
|
|
|
def systemctl(*args):
|
|
return run("systemctl", "--user", *args)[1].strip()
|
|
|
|
|
|
def unix_sockets():
|
|
with open("/proc/net/unix") as f:
|
|
return {parts[7] for parts in map(str.split, f) if len(parts) > 7}
|
|
|
|
|
|
def is_elf(path):
|
|
try:
|
|
with open(path, "rb") as f:
|
|
return f.read(4) == b"\x7fELF"
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
# --- versions
|
|
|
|
def current_versions():
|
|
versions = {}
|
|
with open("/etc/os-release") as f:
|
|
for line in f:
|
|
if line.startswith("BUILD_ID="):
|
|
versions["SteamOS build"] = line.split("=", 1)[1].strip().strip('"')
|
|
for line in run("pacman", "-Q", *PACKAGES)[1].splitlines():
|
|
name, _, version = line.partition(" ")
|
|
if name in PACKAGES:
|
|
versions[name] = version
|
|
versions["kernel"] = platform.release()
|
|
return versions
|
|
|
|
|
|
def check_versions(versions):
|
|
try:
|
|
with open(KNOWN_GOOD) as f:
|
|
good = json.load(f)
|
|
date, old = good["date"], good["versions"]
|
|
except (OSError, ValueError, KeyError, TypeError):
|
|
report("warn", "known-good versions", "none recorded yet; run scripts/doctor.sh --mark-good "
|
|
"once Frametop works")
|
|
return
|
|
changed = [k for k in sorted(set(versions) | set(old)) if old.get(k) != versions.get(k)]
|
|
if not changed:
|
|
report("ok", "versions", f"the same as when marked good ({date})")
|
|
for k in changed:
|
|
hint = PACKAGES.get(k) or (KERNEL_HINT if k == "kernel" else "")
|
|
report("warn", f"{k} changed since {date}",
|
|
f"{old.get(k, 'none')} -> {versions.get(k, 'none')}" + (f"; try {hint}" if hint else ""))
|
|
|
|
|
|
# --- the host, without SteamVR
|
|
|
|
def check_host():
|
|
needed = [
|
|
("/usr/bin/kwin_wayland_wrapper", "FAIL", "the desktop can't start KWin"),
|
|
("/usr/bin/startplasma-wayland", "FAIL", "the desktop can't start Plasma"),
|
|
("/usr/bin/dbus-run-session", "FAIL", "the desktop can't start its session bus"),
|
|
("/usr/bin/python3", "warn", "nested accessibility can't run its startup helper"),
|
|
("/usr/bin/gdbus", "warn", "nested accessibility can't discover or check its bus"),
|
|
("/usr/lib/at-spi-bus-launcher", "warn", "nested accessibility can't start its bus"),
|
|
("/usr/lib/at-spi2-registryd", "warn", "nested accessibility has no fallback registry"),
|
|
("/usr/share/dbus-1/services/org.a11y.Bus.service", "warn",
|
|
"nested accessibility can't activate its bus"),
|
|
("/usr/share/dbus-1/accessibility-services/org.a11y.atspi.Registry.service", "warn",
|
|
"nested accessibility can't activate its registry natively"),
|
|
(f"{STEAMVR_BIN}/vrcmd", "FAIL", "the 3D mouse can't find panels"),
|
|
(f"{STEAMVR_BIN}/vrpathreg", "warn", "the pointer driver can't be installed or removed"),
|
|
("/usr/share/deckard/mesavars.sh", "warn", "the desktop starts without SteamOS's Mesa settings"),
|
|
("/etc/profile.d/flatpak.sh", "warn", "Flatpak apps may open Discover instead of starting"),
|
|
("/usr/share/applications/deckard-nested-desktop.desktop", "warn",
|
|
"SteamOS's Desktop launcher entry is gone or renamed, so Frametop's copy may not replace it"),
|
|
]
|
|
missing = [n for n in needed if not os.path.exists(n[0])]
|
|
for path, state, effect in missing:
|
|
report(state, f"{path} missing", effect)
|
|
if not missing:
|
|
report("ok", "host files", f"all {len(needed)} the desktop and pointer use are there")
|
|
|
|
try:
|
|
with open("/usr/bin/kwin_wayland", "rb") as f:
|
|
kwin = f.read()
|
|
except OSError:
|
|
kwin = b""
|
|
# Qt keeps the option name as a UTF-16 string literal.
|
|
if "output-count".encode("utf-16-le") in kwin:
|
|
report("ok", "KWin", "has --output-count (one output per screen)")
|
|
else:
|
|
report("FAIL", "KWin", "no --output-count option: the desktop gets one screen at most")
|
|
# The session turns these built-in effects off by id (blurEnabled, contrastEnabled in kwinrc).
|
|
gone = [e for e in ("blur", "contrast") if b"KWin::%s_factory" % e.encode() not in kwin]
|
|
if not gone:
|
|
report("ok", "KWin effects", "blur and contrast, which the desktop turns off, keep their ids")
|
|
else:
|
|
report("warn", "KWin effects", f"no built-in {', '.join(gone)} effect: renamed? The desktop's "
|
|
"kwinrc may no longer turn it off (session/frametop-session.sh)")
|
|
|
|
if systemctl("cat", "steamvr.service"):
|
|
report("ok", "steamvr.service", "Frametop's services start and stop with it")
|
|
else:
|
|
report("FAIL", "steamvr.service", "gone or renamed; Frametop's services are ordered on it")
|
|
|
|
glibc = os.confstr("CS_GNU_LIBC_VERSION") or ""
|
|
have = tuple(int(x) for x in re.findall(r"\d+", glibc)[:2])
|
|
want = ".".join(map(str, HOST_GLIBC))
|
|
if have >= HOST_GLIBC:
|
|
report("ok", "glibc", f"{glibc}, the pointer driver needs {want}")
|
|
else:
|
|
report("FAIL", "glibc", f"{glibc}, older than the {want} the pointer driver needs")
|
|
|
|
try:
|
|
with open(VRPATHS) as f:
|
|
drivers = json.load(f).get("external_drivers") or []
|
|
except (OSError, ValueError):
|
|
drivers = []
|
|
driver = next((d for d in drivers if os.path.basename(d.rstrip("/")) == "ft_pointer"), None)
|
|
if driver and os.path.isfile(os.path.join(driver, "bin/linuxarm64/driver_ft_pointer.so")):
|
|
report("ok", "pointer driver", f"registered with SteamVR ({driver})")
|
|
else:
|
|
report("FAIL", "pointer driver", "not registered with SteamVR; run pointer/driver/install.sh install, "
|
|
"then restart SteamVR")
|
|
try:
|
|
with open(STRAY_VRPATHS) as f:
|
|
stray = json.load(f).get("runtime") is None
|
|
except (OSError, ValueError, AttributeError):
|
|
stray = False
|
|
if stray:
|
|
report("warn", "OpenVR path registry", f"{STRAY_VRPATHS} has no SteamVR in it, and hides SteamVR from "
|
|
"OpenVR programs started in the Frametop desktop; pointer/driver/install.sh install removes it")
|
|
|
|
session = launcher_session()
|
|
if session is None:
|
|
report("warn", "launcher", "Launch a program -> Desktop starts the stock desktop "
|
|
"(./desktops.sh install brings Frametop back)")
|
|
elif os.path.isfile(session):
|
|
report("ok", "launcher", f"Desktop starts {session}")
|
|
else:
|
|
report("FAIL", "launcher", f"Desktop starts {session}, which doesn't exist")
|
|
|
|
if systemctl("is-enabled", "frametop-power") == "enabled":
|
|
if os.access(BACKLIGHT, os.W_OK):
|
|
report("ok", "backlight", "ft-powerd can turn the displays off")
|
|
else:
|
|
report("FAIL", "backlight", f"{BACKLIGHT} isn't writable, so ft-powerd can't turn the displays off")
|
|
|
|
|
|
def launcher_session():
|
|
try:
|
|
with open(LAUNCHER) as f:
|
|
m = re.search(r"^Exec=(\S+)", f.read(), re.M)
|
|
except OSError:
|
|
return None
|
|
return m.group(1) if m else None
|
|
|
|
|
|
# --- with SteamVR running
|
|
|
|
def installed_binaries():
|
|
"""The programs the services and the launcher run: {path: what runs it}."""
|
|
found = {}
|
|
for unit in UNITS:
|
|
argv = re.search(r"argv\[\]=([^;]*)", systemctl("show", "-p", "ExecStart", "--value", unit + ".service"))
|
|
for arg in (argv.group(1).split() if argv else []):
|
|
if not arg.startswith(HOME) or not os.path.isfile(arg):
|
|
continue
|
|
if is_elf(arg):
|
|
found[arg] = unit
|
|
else:
|
|
# A script runs the programs built next to it (ft-gazed runs build/ft-gaze).
|
|
build = os.path.join(os.path.dirname(arg), "build")
|
|
for name in sorted(os.listdir(build)) if os.path.isdir(build) else []:
|
|
if is_elf(os.path.join(build, name)):
|
|
found[os.path.join(build, name)] = unit
|
|
session = launcher_session()
|
|
if session:
|
|
screens = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(session)),
|
|
"../screens/build/ft-screens"))
|
|
if os.path.isfile(screens):
|
|
found[screens] = "the desktop"
|
|
return found
|
|
|
|
|
|
def check_openvr():
|
|
needs = {} # interface version: programs built against it
|
|
for path in installed_binaries():
|
|
with open(path, "rb") as f:
|
|
for name in set(re.findall(rb"IVR[A-Za-z]+_\d{3}", f.read())):
|
|
needs.setdefault(name.decode(), []).append(os.path.basename(path))
|
|
if not needs:
|
|
report("skip", "OpenVR interfaces", "no installed Frametop programs found")
|
|
return
|
|
code, out = run(sys.executable, "-c", OPENVR_PROBE, f"{STEAMVR_BIN}/libopenvr_api.so", *sorted(needs),
|
|
timeout=30)
|
|
served = dict(line.split() for line in out.splitlines() if len(line.split()) == 2)
|
|
if code != 0 or "init" in served:
|
|
why = served.get("init") or out.strip() or f"exit {code}"
|
|
report("FAIL", "OpenVR", f"can't connect to SteamVR as a background app ({why})")
|
|
return
|
|
gone = [n for n in sorted(needs) if served.get(n) != "1"]
|
|
for name in gone:
|
|
report("FAIL", f"OpenVR {name}", f"this SteamVR doesn't serve it; rebuild {', '.join(sorted(needs[name]))} "
|
|
"against a newer OpenVR header")
|
|
if not gone:
|
|
programs = sorted({p for ps in needs.values() for p in ps})
|
|
report("ok", "OpenVR interfaces", f"all {len(needs)} that {', '.join(programs)} use are served")
|
|
|
|
|
|
def check_overlays(desktop_up):
|
|
code, out = run(f"{STEAMVR_BIN}/vrcmd", "--overlays",
|
|
env=dict(os.environ, LD_LIBRARY_PATH=STEAMVR_BIN))
|
|
# The format ft-pointer.cpp and ft_layout.py parse: 'key' -- 'name', WxH visible VROverlayType_...
|
|
keys = re.findall(r"^'([^']+)' -- '.*VROverlayType_", out, re.M)
|
|
if not keys:
|
|
report("FAIL", "vrcmd --overlays", "lists no overlays in the expected format, so the 3D mouse can't "
|
|
"find panels (pointer/helper/ft-pointer.cpp and layout/ft_layout.py parse it)")
|
|
elif desktop_up and not any(re.fullmatch(r"frametop\.screen\.\d+", k) for k in keys):
|
|
report("FAIL", "vrcmd --overlays", f"lists {len(keys)} overlays, but none of the desktop's screens")
|
|
else:
|
|
report("ok", "vrcmd --overlays", f"{len(keys)} overlays in the format the pointer parses")
|
|
|
|
|
|
def check_services(sockets, desktop_up):
|
|
try:
|
|
with open(PAUSE_STATE) as f:
|
|
pause = json.load(f)
|
|
paused = pause["stopped"]["units"] if pause.get("paused") else []
|
|
except (OSError, ValueError, KeyError, TypeError):
|
|
paused = []
|
|
for unit, socket in UNITS.items():
|
|
if systemctl("is-enabled", unit) != "enabled":
|
|
continue
|
|
state = systemctl("is-active", unit)
|
|
if state != "active" and unit + ".service" in paused:
|
|
report("skip", unit, "stopped while Frametop is paused for a VR game (input/ft-pause off resumes it)")
|
|
elif state != "active":
|
|
report("FAIL", unit, f"{state}; see journalctl --user -u {unit}")
|
|
elif socket and socket not in sockets:
|
|
report("FAIL", unit, f"runs, but hasn't opened {socket}")
|
|
else:
|
|
report("ok", unit, "running")
|
|
if "@ft_pointer" in sockets:
|
|
report("ok", "ft_pointer driver", "SteamVR loaded it")
|
|
else:
|
|
report("FAIL", "ft_pointer driver", "SteamVR didn't load it; see pointer/driver/install.sh log")
|
|
if not desktop_up:
|
|
report("skip", "desktop", "not running; start it and check again to cover the screens")
|
|
elif "@ft_screens" in sockets:
|
|
report("ok", "desktop", "ft-screens is running")
|
|
else:
|
|
report("FAIL", "desktop", "ft-screens runs, but hasn't opened @ft_screens")
|
|
if systemctl("is-enabled", "frametop-camd") == "enabled" and run("pgrep", "-x", "XRService")[0] != 0:
|
|
report("FAIL", "XRService", "not running, so hand tracking has no cameras")
|
|
|
|
|
|
def check_steam_ui():
|
|
"""Steam's UI calls the Steam menu shortcut uses (steam/ft-steam). They're Steam client
|
|
internals, so a Steam update can change them too, not just a SteamOS one."""
|
|
# Run from stdin (over SSH) there's no __file__: try the usual checkouts.
|
|
repos = [os.path.dirname(os.path.dirname(os.path.abspath(__file__)))] if "__file__" in globals() else []
|
|
repos += [os.path.join(HOME, "frametop"), os.path.join(HOME, "dev/frametop")]
|
|
tool = next((t for t in (os.path.join(r, "steam/ft-steam") for r in repos) if os.path.exists(t)), None)
|
|
if not tool:
|
|
report("skip", "Steam menu shortcut", "steam/ft-steam isn't in a checkout here")
|
|
return
|
|
try:
|
|
p = subprocess.run([tool, "check"], capture_output=True, text=True, timeout=15)
|
|
except (OSError, subprocess.TimeoutExpired) as e:
|
|
report("warn", "Steam menu shortcut", f"ft-steam check didn't run ({e})")
|
|
return
|
|
detail = (p.stdout + p.stderr).strip()
|
|
if p.returncode == 0:
|
|
report("ok", "Steam menu shortcut", "Steam's UI still has the calls steam/ft-steam makes")
|
|
elif detail.startswith("ft-steam:"):
|
|
report("skip", "Steam menu shortcut", detail[len("ft-steam:"):].strip())
|
|
else:
|
|
report("warn", "Steam menu shortcut", f"{detail}; a Meta tap won't open the Steam menu (steam/ft-steam)")
|
|
|
|
|
|
def check_vr_socket():
|
|
"""vrserver's web socket, which the pause gesture reads the controllers from (input/vrws.py,
|
|
input/game_pause.py). It's undocumented: SteamVR's controller binding page uses it."""
|
|
label, headers = "vrserver web socket", {"Referer": f"{VRSERVER}/dashboard/controllerbinding.html"}
|
|
try:
|
|
import urllib.request
|
|
with urllib.request.urlopen(urllib.request.Request(f"{VRSERVER}/input/getstate.json", headers=headers),
|
|
timeout=3) as r:
|
|
devices = json.load(r).get("devices", [])
|
|
except (OSError, ValueError) as e:
|
|
report("FAIL", label, f"/input/getstate.json didn't answer ({e}); the pause gesture can't read the controllers")
|
|
return
|
|
found = [d for d in devices if d.get("controller_type") == "frame_controller" and d.get("root_path")
|
|
and d.get("side") in ("left", "right")]
|
|
if not found:
|
|
report("skip", label, "no Frame controller is on, so it wasn't checked")
|
|
return
|
|
if not all(any(c.get("path") == "/input/thumbstick/click" for c in d.get("components", [])) for d in found):
|
|
report("warn", label, "a controller lists no /input/thumbstick/click; the default pause gesture needs it")
|
|
data = b""
|
|
try:
|
|
with socket.create_connection(("127.0.0.1", 27062), timeout=3) as s:
|
|
key = base64.b64encode(os.urandom(16)).decode()
|
|
s.sendall((f"GET / HTTP/1.1\r\nHost: 127.0.0.1:27062\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
|
|
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\nOrigin: {VRSERVER}\r\n"
|
|
f"Referer: {headers['Referer']}\r\n\r\n").encode())
|
|
while b"\r\n\r\n" not in data:
|
|
chunk = s.recv(4096)
|
|
if not chunk:
|
|
break
|
|
data += chunk
|
|
if b" 101 " not in data.split(b"\r\n", 1)[0] + b" ":
|
|
report("FAIL", label, "vrserver refused the web socket; the pause gesture can't read the controllers")
|
|
return
|
|
mailbox = f"frametop_check_{os.getpid()}"
|
|
# Every controller: one lying still (or asleep) may send nothing at all.
|
|
for text in [f"mailbox_open {mailbox}"] + ["mailbox_send input_server " + json.dumps(
|
|
{"type": "request_input_state_updates", "device_path": d["root_path"], "returnAddress": mailbox})
|
|
for d in found]:
|
|
payload, mask = text.encode(), os.urandom(4)
|
|
size = bytes([0x80 | len(payload)]) if len(payload) < 126 else bytes([0x80 | 126]) + struct.pack(">H", len(payload))
|
|
s.sendall(b"\x81" + size + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(payload)))
|
|
deadline = time.monotonic() + 3
|
|
try:
|
|
while b"update_component_states" not in data and time.monotonic() < deadline:
|
|
chunk = s.recv(65536)
|
|
if not chunk:
|
|
break
|
|
data += chunk
|
|
except socket.timeout:
|
|
report("skip", label, "it took the subscription, but no controller sent anything in 3 s "
|
|
"(lying still or asleep?): pick one up and check again")
|
|
return
|
|
except OSError as e:
|
|
report("FAIL", label, f"{e}; the pause gesture can't read the controllers")
|
|
return
|
|
if b"update_component_states" in data and b"/click" in data:
|
|
report("ok", label, "it still streams the controllers' buttons, which the pause gesture reads")
|
|
else:
|
|
report("FAIL", label, "no button states came, so the pause gesture can't read the controllers "
|
|
"(input/vrws.py: the message format changed?)")
|
|
|
|
|
|
def check_eye_tracker():
|
|
gaze = systemctl("is-enabled", "frametop-gaze") == "enabled"
|
|
try:
|
|
with open(EYE_MMAP, "rb") as f:
|
|
m = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ)
|
|
except (OSError, ValueError):
|
|
report("FAIL" if gaze else "skip", "eye tracker", f"{EYE_MMAP} isn't there")
|
|
return
|
|
if len(m) < EYE_NEED:
|
|
report("FAIL", "eye tracker", f"{EYE_MMAP} is {len(m)} bytes, smaller than gaze/ft-gaze.cpp reads")
|
|
return
|
|
first = struct.unpack_from("<I", m, EYE_COUNTER)[0]
|
|
time.sleep(0.3)
|
|
if struct.unpack_from("<I", m, EYE_COUNTER)[0] == first:
|
|
report("skip", "eye tracker", "idle, so its layout wasn't checked")
|
|
return
|
|
age = time.clock_gettime(time.CLOCK_MONOTONIC_RAW) - struct.unpack_from("<d", m, EYE_TIME)[0]
|
|
units = sum(abs(math.sqrt(sum(x * x for x in struct.unpack_from("<3f", m, o))) - 1) < 0.02
|
|
for o in EYE_VECTORS)
|
|
# A lost eye can zero its vector, so two of the four are enough. The timestamp is the
|
|
# strong check: a fresh CLOCK_MONOTONIC_RAW double doesn't land on that offset by chance.
|
|
if abs(age) < 1 and units >= 2:
|
|
report("ok", "eye tracker", "eye-server.mmap still has the layout gaze/ft-gaze.cpp reads")
|
|
else:
|
|
report("FAIL" if gaze else "warn", "eye tracker", f"eye-server.mmap layout changed (sample age "
|
|
f"{age:.3g} s, {units} of 4 gaze vectors unit length); update the offsets in gaze/ft-gaze.cpp")
|
|
|
|
|
|
def main():
|
|
args = sys.argv[1:]
|
|
if args not in ([], ["--mark-good"]):
|
|
sys.exit("usage: update-check.py [--mark-good]")
|
|
# A terminal in the desktop has its session's runtime dir and bus; systemctl needs the real ones.
|
|
os.environ["XDG_RUNTIME_DIR"] = f"/run/user/{UID}"
|
|
os.environ["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path=/run/user/{UID}/bus"
|
|
# And its own config folder, where SteamVR's path registry isn't: OpenVR and vrcmd need ours.
|
|
os.environ["XDG_CONFIG_HOME"] = os.path.join(HOME, ".config")
|
|
|
|
versions = current_versions()
|
|
check_versions(versions)
|
|
check_host()
|
|
steamvr_up = run("pgrep", "-x", "vrserver")[0] == 0
|
|
if not steamvr_up:
|
|
report("skip", "SteamVR checks", "SteamVR isn't running")
|
|
else:
|
|
sockets = unix_sockets()
|
|
desktop_up = run("pgrep", "-x", "ft-screens")[0] == 0
|
|
check_openvr()
|
|
check_overlays(desktop_up)
|
|
check_services(sockets, desktop_up)
|
|
check_steam_ui()
|
|
check_vr_socket()
|
|
check_eye_tracker()
|
|
|
|
if args == ["--mark-good"]:
|
|
if failed:
|
|
print("Not recording these versions as working: fix the failures first.")
|
|
elif not steamvr_up:
|
|
print("Not recording these versions as working: start SteamVR, so its checks run too.")
|
|
sys.exit(1)
|
|
else:
|
|
os.makedirs(os.path.dirname(KNOWN_GOOD), exist_ok=True)
|
|
with open(KNOWN_GOOD, "w") as f:
|
|
json.dump({"date": time.strftime("%Y-%m-%d"), "versions": versions}, f, indent=2)
|
|
print(f"Recorded these versions as working in {KNOWN_GOOD}.")
|
|
sys.exit(1 if failed else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|