mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 05:00:08 +02:00
[verified] merge experimental into nested AT-SPI fix
This commit is contained in:
commit
5e3323c188
143 files changed
+20723
-841
No files matched your search
+18
-3
@@ -6,7 +6,22 @@
|
||||
# `distrobox enter`.
|
||||
box=${FRAME_BOX:-dev}
|
||||
export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
|
||||
[ "$(podman container inspect -f '{{.State.Running}}' "$box" 2>/dev/null)" = true ] && exit 0
|
||||
running() { [ "$(podman container inspect -f '{{.State.Running}}' "$box" 2>/dev/null)" = true ]; }
|
||||
running && exit 0
|
||||
podman container exists "$box" 2>/dev/null || exit 0 # not created yet: setup/dev-container.sh does that
|
||||
exec systemd-run --user --scope --quiet --collect --description="$box container (started for Frametop)" \
|
||||
podman start "$box" >/dev/null
|
||||
since=$(date -u +%FT%T)
|
||||
systemd-run --user --scope --quiet --collect --description="$box container (started for Frametop)" \
|
||||
podman start "$box" >/dev/null || exit
|
||||
|
||||
# Every start runs distrobox-init in the container, and `distrobox enter` only waits for it
|
||||
# when it starts the container itself. The first start installs what distrobox needs and sets
|
||||
# up passwordless sudo, which takes a minute or more; until then sudo in the container asks
|
||||
# for a password (issue #9). Later starts take a few seconds.
|
||||
for i in $(seq 600); do
|
||||
podman logs --since "$since" "$box" 2>&1 | grep -q '^container_setup_done' && exit 0
|
||||
running || break
|
||||
[ "$i" = 10 ] && echo "setting up the $box container (the first start takes a few minutes)" >&2
|
||||
sleep 1
|
||||
done
|
||||
echo "the $box container didn't finish starting; see: podman logs $box" >&2
|
||||
exit 1
|
||||
@@ -36,6 +36,10 @@ check "distrobox" on_frame 'test -x ~/.local/bin/distrobox && ~/.local/bin/distr
|
||||
check "container $FRAME_BOX" on_frame "podman ps -a --filter name=^$FRAME_BOX\$ --format '{{.Image}} {{.Status}}' | grep ."
|
||||
check "repo on the Frame" on_frame 'pwd'
|
||||
check "free space in ~" on_frame "df -h ~ | awk 'NR==2{print \$4\" free\"}'"
|
||||
# A taskbar saved on a screen the desktop doesn't have is hidden; the desktop's next start
|
||||
# moves it to the first screen (session/fix-panels.py).
|
||||
check "taskbar on a screen" on_frame 'set -o pipefail; [ -f session/fix-panels.py ] || { echo "not checked (older checkout)"; exit 0; }
|
||||
python3 session/fix-panels.py --check | paste -sd ";" | sed "s/;/; /g"'
|
||||
|
||||
echo "what Frametop needs from SteamOS:"
|
||||
if [ "$FRAME_LOCAL" = 1 ]; then
|
||||
|
||||
@@ -112,7 +112,7 @@ def main():
|
||||
try:
|
||||
with open(os.path.join(HOME, ".config/frametop.conf")) as f:
|
||||
conf = [ln.split("#")[0].strip() for ln in f]
|
||||
out("Settings: " + " ".join(c for c in conf if re.match(r"(POINTER|META_DASHBOARD|SHARE_KEYS|BACKEND)=", c)))
|
||||
out("Settings: " + " ".join(c for c in conf if re.match(r"(POINTER|SHARE_KEYS|BACKEND)=", c)))
|
||||
except OSError:
|
||||
out("Settings: no ~/.config/frametop.conf")
|
||||
out("ft-screens state (visibility, manual, wrist, gesture, lasers, game running, in games): "
|
||||
|
||||
@@ -25,6 +25,7 @@ for u in frametop-input-relay frametop-pointer frametop-power; do
|
||||
echo "$u: $(systemctl --user is-enabled $u 2>/dev/null) / $(systemctl --user is-active $u 2>/dev/null)"
|
||||
done
|
||||
echo "desktop: $(pgrep -x ft-screens >/dev/null && echo running || echo 'not running'), plasmashell: $(pgrep -c plasmashell || true)"
|
||||
echo "paused for VR games: $(cat /run/user/$(id -u)/frametop-pause.json 2>/dev/null || echo 'no (never paused since boot)')"
|
||||
LD_LIBRARY_PATH=/opt/steamvr/bin/linuxarm64 /opt/steamvr/bin/linuxarm64/vrpathreg show 2>/dev/null | sed -n '/xternal/,$p'
|
||||
|
||||
section "What Frametop needs from SteamOS (scripts/update-check.py)"
|
||||
@@ -41,6 +42,25 @@ for i, s in enumerate(d.get("screens", []), 1):
|
||||
print("visibility:", d.get("visibility"))
|
||||
PY
|
||||
|
||||
section "Plasma panels and outputs"
|
||||
python3 "$repo/session/fix-panels.py" --check 2>&1 || true
|
||||
python3 - "$repo" <<'PY' 2>&1 || true
|
||||
import json, subprocess, sys
|
||||
sys.path.insert(0, sys.argv[1] + "/layout")
|
||||
import ft_layout
|
||||
env = ft_layout.nested_env()
|
||||
if not env:
|
||||
sys.exit(print("desktop not running: no live outputs or panels"))
|
||||
outs = json.loads(subprocess.run(["kscreen-doctor", "-j"], capture_output=True, text=True, env=env, timeout=10).stdout or "{}")
|
||||
for o in outs.get("outputs", []):
|
||||
size = o.get("size") or {}
|
||||
print(f"output {o.get('name')}: enabled={o.get('enabled')} priority={o.get('priority')} {size.get('width')}x{size.get('height')}")
|
||||
js = "print(JSON.stringify(panels().map(p => ({id: p.id, screen: p.screen, location: p.location}))))"
|
||||
r = subprocess.run(["qdbus6", "org.kde.plasmashell", "/PlasmaShell", "org.kde.PlasmaShell.evaluateScript", js],
|
||||
capture_output=True, text=True, env=env, timeout=10)
|
||||
print("live panels:", (r.stdout or r.stderr).strip())
|
||||
PY
|
||||
|
||||
section "Input relay (last 60 lines)"
|
||||
journalctl --user -u frametop-input-relay -n 60 --no-pager -o short 2>/dev/null
|
||||
section "Pointer helper (last 60 lines)"
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
"$root/scripts/frame.sh" -C screens 'mkdir -p build; gcc -std=c11 -Wall -Wextra -Werror tests/controller-click-test.c -lm -o build/controller-click-test && build/controller-click-test'
|
||||
+119
-9
@@ -12,15 +12,18 @@ This checks each one, and compares the versions with the last ones recorded as w
|
||||
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, and maps the eye tracker's shared
|
||||
memory read-only. It never starts, stops, or restarts anything.
|
||||
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
|
||||
@@ -34,6 +37,8 @@ LAUNCHER = os.path.join(HOME, ".local/share/applications/deckard-nested-desktop.
|
||||
VRPATHS = os.path.join(HOME, ".config/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.
|
||||
@@ -41,8 +46,9 @@ 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",
|
||||
"plasma-workspace": "the taskbar and panels after a desktop restart",
|
||||
"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",
|
||||
@@ -181,14 +187,21 @@ def check_host():
|
||||
|
||||
try:
|
||||
with open("/usr/bin/kwin_wayland", "rb") as f:
|
||||
# Qt keeps the option name as a UTF-16 string literal.
|
||||
output_count = "output-count".encode("utf-16-le") in f.read()
|
||||
kwin = f.read()
|
||||
except OSError:
|
||||
output_count = False
|
||||
if output_count:
|
||||
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")
|
||||
@@ -307,11 +320,19 @@ def check_overlays(desktop_up):
|
||||
|
||||
|
||||
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":
|
||||
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}")
|
||||
@@ -331,6 +352,93 @@ def check_services(sockets, desktop_up):
|
||||
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:
|
||||
@@ -379,6 +487,8 @@ def main():
|
||||
check_openvr()
|
||||
check_overlays(desktop_up)
|
||||
check_services(sockets, desktop_up)
|
||||
check_steam_ui()
|
||||
check_vr_socket()
|
||||
check_eye_tracker()
|
||||
|
||||
if args == ["--mark-good"]:
|
||||
|
||||
Reference in new issue
Block a user