diff --git a/docs/webxr-chromium.md b/docs/webxr-chromium.md index df151fc..536c5b6 100644 --- a/docs/webxr-chromium.md +++ b/docs/webxr-chromium.md @@ -3,9 +3,11 @@ Goal: open a web VR180 or 360 player (DeoVR and DL8 embeds, WebXR samples), press its VR button, and watch in 3D in the headset. -A standalone, public version of this build (build script, the SO_PEERCRED -patch, and a Frame-side installer that adds "Chromium XR" to the Steam library) -is at [saphid/chromium-webxr-steam-frame](https://github.com/saphid/chromium-webxr-steam-frame). +The build and installer now live in their own public repo, +[saphid/chromium-webxr-steam-frame](https://github.com/saphid/chromium-webxr-steam-frame): +a build script for an x86-64 Linux host, the SO_PEERCRED patch, and a +Frame-side installer that adds "Chromium XR" to the Steam library. This page +keeps the findings and what was verified on this Frame. ## Why Flathub Chromium can't @@ -45,76 +47,53 @@ gets both. SteamVR (`bin/linuxarm64/vrclient.so`, `VALVE_runtime_is_steamvr`). The Linux backend uses Vulkan (`XR_USE_GRAPHICS_API_VULKAN`). -## Building it +## Building and installing it -[`scripts/build-chromium-xr.sh`](../scripts/build-chromium-xr.sh) -cross-compiles arm64 Linux Chromium on an x64 Linux host. It doesn't need -sudo: the arm64 sysroot comes from Chromium's own script. It needs about -90 GB of disk. It shallow-fetches the CL ref (patchset 44), runs -`gclient sync --no-history`, installs the sysroot, applies one extra seccomp -fix (below), builds `chrome` with `symbol_level=0` and proprietary codecs, and -packs `chromium-xr-arm64.tar.xz` (about 145 MB, GPU libraries included). -Progress is logged to `~/chromium-xr/stage`. The build aborts if the disk -holding `~/chromium-xr` drops below 12 GB free. +Follow the [public repo's README](https://github.com/saphid/chromium-webxr-steam-frame#build). +In short: `build/build.sh` on an x64 Linux host (no sudo, about 90 GB of +disk) produces `chromium-xr-arm64.tar.xz` (about 145 MB), and +`frame/install.sh` on the Frame unpacks it to `~/chromium-xr`, installs the +`chromium-xr` launcher in `~/.local/bin`, and adds the Steam library shortcut +through the Steam client's DevTools port, the same way as T3 Code +([apks.md](apks.md)). Launching the shortcut gives Chromium its own panel, +`valve.steam.desktopgame.`, like any other app. -First run, 2026-09-25, on a 12-core, 31 GB x64 Linux box: 9 h 33 min for +First build, 2026-09-25, on a 12-thread, 31 GB x64 Linux box: 9 h 33 min for 94,835 steps, giving Chromium 156.0.8071.0. A rebuild after a one-file change takes under a minute, plus about 4 minutes to repack. -**The extra fix.** The CL's XR seccomp policy refuses `getsockopt`. SteamVR's +To debug from the Mac, start it with DevTools on the Frame: +`ssh frame 'DISPLAY=:0 ~/.local/bin/chromium-xr --remote-debugging-port=9223 URL'`, or +launch it as a panel with +`scripts/panel-on-frame.sh -- '~/.local/bin/chromium-xr' --remote-debugging-port=9223 URL` +([panels.md](panels.md)). DevTools has no authentication. It listens on +loopback, but with the userspace Tailscale from [tailscale.md](tailscale.md) +running, loopback ports are reachable from your tailnet. Close the browser +when you're done. Chromium runs one browser per profile, so close the +Steam-launched one first or the flag is ignored. + +**The SO_PEERCRED fix.** The XR seccomp policy refuses `getsockopt`. SteamVR's client calls `getsockopt(SOL_SOCKET, SO_PEERCRED)` inside `xrCreateInstance`, -so the XR process died with a seccomp crash (arm64 syscall 209). The script -allows that one option. That's needed but not enough: `launch` still turns +so the XR process died with a seccomp crash (arm64 syscall 209). The patch +allows that one option. It's needed but not enough: the launcher still turns seccomp off (below), so the patch only matters once that's fixed too. -## Running it on the Frame +**Seccomp is off.** The launcher passes `--disable-seccomp-filter-sandbox`. +With the XR seccomp policy on, SteamVR's client reads `/proc/self/status` +through Chrome's file broker and gets the broker's pid. SteamVR then binds +the app to the wrong process ("Unable to init path manager: +VRInitError_Init_Internal") and `xrCreateInstance` fails. The broker can't +answer `/proc/self` for another process, so fixing this needs a change in +Chromium's broker client or in the CL. The namespace sandbox stays on, but +seccomp is off for every process, so use this profile for VR sites rather +than everyday browsing. -[`scripts/chromium-xr.sh`](../scripts/chromium-xr.sh): - -```sh -BUILD_HOST=my-linux-box scripts/chromium-xr.sh install # your build host; scp, unpack to ~/chromium-xr -scripts/chromium-xr.sh launch [URL] # its own VR panel, --enable-features=OpenXR -scripts/chromium-xr.sh steam # adds "Chromium XR" to the Steam library -scripts/chromium-xr.sh check # prints isSessionSupported('immersive-vr') -``` - -It runs natively, not as a Flatpak. `launch` opens it as its own panel on -gamescope's X display, the same way as [`panel-on-frame.sh`](../scripts/panel-on-frame.sh) ([panels.md](panels.md)), so -the Plasma desktop doesn't need to be open. It uses its own profile -(`~/.config/chromium-xr`) and DevTools on loopback port 9223, so it doesn't -collide with the Flatpak's 9222. When a page enters VR, Chrome asks -**Allow VR?** in the browser panel; choose *Allow this time* or *Allow while -visiting the site*. - -Both ways of starting it run -[`frame/chromium-xr/launch.sh`](../frame/chromium-xr/launch.sh), copied to -`~/Applications/ChromiumXR/launch.sh` on the Frame, which holds Chrome's flags. -It sits outside `~/chromium-xr` so `install` doesn't delete it. - -**From the Steam library (verified 2026-09-26).** `steam` adds a non-Steam -shortcut called "Chromium XR" (with the Flathub Chromium icon, if that's -installed) through the Steam client's DevTools port, the same way as T3 Code -([apks.md](apks.md)), without restarting Steam. It saves the app id in -`~/Applications/ChromiumXR/shortcut-appid`, so rerunning it, even after you -rename the shortcut in the library, doesn't add a second one. On this Frame -the shortcut app id is 2240749789. Launching it from the library gives -Chromium its own panel, `valve.steam.desktopgame.2240749789`, like any other -app. A Steam launch doesn't open a DevTools port, so `check` needs `launch`. -Chromium runs one browser per profile: while the Steam-launched one is open, -`launch` opens its URL in that window, without DevTools, then prints -`failed: ... exited` because no new window appeared. Close it first. - -**Seccomp is off.** The wrapper passes `--disable-seccomp-filter-sandbox`. With -the XR seccomp policy on, SteamVR's client reads `/proc/self/status` through -Chrome's file broker and gets the broker's pid. SteamVR then binds the app to -the wrong process ("Unable to init path manager: VRInitError_Init_Internal") -and `xrCreateInstance` fails. The broker can't answer `/proc/self` for another -process, so fixing this needs a change in Chromium's broker client or in the -CL. The namespace sandbox stays on, but seccomp is off for every process, so -use this profile for VR sites rather than everyday browsing. DevTools on -port 9223 has no authentication. It listens on loopback, but with the -userspace Tailscale from [tailscale.md](tailscale.md) running, loopback ports -are reachable from your tailnet. Close the browser when you're done. +**Upstream (2026-09-27).** CL 8441736 (the XR sandbox) has merged into +Chromium, still refusing `getsockopt`; CL 8132979 is still in review. Valve +and the CLs' author are working on Steam Frame support +([utzcoz/chromium-webxr-linux#5](https://github.com/utzcoz/chromium-webxr-linux/issues/5)). +Both sandbox problems above, with the patch, are reported in +[utzcoz/chromium-webxr-linux#7](https://github.com/utzcoz/chromium-webxr-linux/issues/7). **Verified 2026-09-26** (Frame BUILD_ID 20260922.6101926, SteamVR 2.17.10, this build): diff --git a/frame/chromium-xr/launch.sh b/frame/chromium-xr/launch.sh deleted file mode 100755 index f31e027..0000000 --- a/frame/chromium-xr/launch.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# Frame-side: start the WebXR Chromium build (~/chromium-xr). The Steam -# library shortcut "Chromium XR" runs this, and so does -# `scripts/chromium-xr.sh launch` (which adds a DevTools port). Extra -# arguments go to Chrome, so a URL opens that page. -# -# Lives in ~/Applications/ChromiumXR, outside ~/chromium-xr, so reinstalling -# the build doesn't delete it. -set -euo pipefail - -CHROME="$HOME/chromium-xr/chrome" -[[ -x "$CHROME" ]] || { echo "launch.sh: no build at $CHROME (run chromium-xr.sh install)" >&2; exit 1; } - -# Without --no-first-run and --password-store=basic, startup can stop at a -# first-run or keyring prompt. -# --disable-seccomp-filter-sandbox: under the XR seccomp policy, SteamVR's -# client reads /proc/self/status through the file broker, gets the broker's -# pid, and SteamVR binds the app to the wrong process, so xrCreateInstance -# fails. The namespace sandbox stays on, but seccomp is off for every -# process, so keep this profile for VR sites. -exec "$CHROME" \ - --user-data-dir="$HOME/.config/chromium-xr" \ - --enable-features=OpenXR \ - --ozone-platform=x11 \ - --no-first-run --no-default-browser-check --password-store=basic \ - --disable-seccomp-filter-sandbox \ - "$@" diff --git a/scripts/build-chromium-xr.sh b/scripts/build-chromium-xr.sh deleted file mode 100755 index 4b1dafb..0000000 --- a/scripts/build-chromium-xr.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -# Linux-side (x64 host): cross-compile arm64 Chromium with the Linux OpenXR CLs -# (8441736 + 8132979, bug 506004811), plus a one-option seccomp fix, so WebXR -# immersive-vr works on the Frame. -# Needs ~90 GB free, no sudo. Takes hours; run it detached on the build host: -# scp scripts/build-chromium-xr.sh buildhost:chromium-xr/build.sh -# ssh buildhost 'cd ~/chromium-xr && tmux new -d -s chromium-xr "./build.sh > build.log 2>&1"' -# Progress: ~/chromium-xr/stage. Output: ~/chromium-xr/chromium-xr-arm64.tar.xz, -# which scripts/chromium-xr.sh install copies to the Frame. -# Re-running resumes: existing checkout and out/XR are reused. -set -euo pipefail -W=~/chromium-xr -cd "$W" -stage(){ echo "$(date -Is) $*" | tee -a "$W/stage"; } -# Returns non-zero below 12 GB free; set -e turns that into an exit at top level. -guard(){ avail=$(df --output=avail -BG "$W" | tail -n 1 | tr -dc 0-9); if [ "$avail" -lt 12 ]; then stage "ABORT: only ${avail}G free for $W"; return 3; fi; } -[ -d depot_tools ] || git clone -q https://chromium.googlesource.com/chromium/tools/depot_tools.git -export PATH="$W/depot_tools:$PATH" DEPOT_TOOLS_UPDATE=1 DEPOT_TOOLS_METRICS=0 -CL_REF=refs/changes/79/8132979/44 -if [ ! -f .gclient ]; then - cat > .gclient <<'G' -solutions = [{ "name": "src", "url": "https://chromium.googlesource.com/chromium/src.git", - "managed": False, "custom_deps": {}, "custom_vars": { "checkout_nacl": False } }] -target_os = ["linux"] -target_cpu = ["arm64"] -G -fi -# Keyed on a real commit, so an interrupted first fetch is retried on re-run. -if ! git -C src rev-parse -q --verify HEAD >/dev/null 2>&1; then - stage "clone src at $CL_REF" - mkdir -p src - [ -d src/.git ] || git -C src init -q - git -C src remote get-url origin >/dev/null 2>&1 || git -C src remote add origin https://chromium.googlesource.com/chromium/src.git - git -C src fetch -q --depth=1 origin "$CL_REF" - git -C src checkout -q FETCH_HEAD -fi -guard -stage "src at $(git -C src log -1 --format='%h %s')" -rev=$(git -C src rev-parse HEAD) -# Sync once per revision: once the patch below is applied, gclient sync -# refuses to run on the modified checkout, so re-runs must skip it. -if [ "$(cat "$W/synced" 2>/dev/null)" != "$rev" ]; then - stage "gclient sync" - gclient sync --nohooks --no-history -D --shallow --revision "src@$rev" -j 8 - guard - stage "runhooks" - gclient runhooks - src/build/linux/sysroot_scripts/install-sysroot.py --arch=arm64 - echo "$rev" > "$W/synced" -fi -guard -cd src -# CL 8441736's XR seccomp policy refuses getsockopt, and SteamVR's IPC client -# calls getsockopt(SO_PEERCRED) inside xrCreateInstance, which crashes the XR -# process (verified on the Frame 2026-09-26). Allow only that option. -IFS= read -r -d '' PEERCRED_PATCH <<'P' || true -diff --git a/sandbox/policy/linux/bpf_xr_policy_linux.cc b/sandbox/policy/linux/bpf_xr_policy_linux.cc -index 435e13d396..297453f582 100644 ---- a/sandbox/policy/linux/bpf_xr_policy_linux.cc -+++ b/sandbox/policy/linux/bpf_xr_policy_linux.cc -@@ -11,6 +11,7 @@ - #include "sandbox/linux/system_headers/linux_syscalls.h" - #include "sandbox/policy/linux/sandbox_linux.h" - -+using sandbox::bpf_dsl::AllOf; - using sandbox::bpf_dsl::Allow; - using sandbox::bpf_dsl::Arg; - using sandbox::bpf_dsl::Error; -@@ -27,8 +28,8 @@ XrProcessPolicy::~XrProcessPolicy() = default; - ResultExpr XrProcessPolicy::EvaluateSyscall(int system_call_number) const { - switch (system_call_number) { - // The runtime reaches its compositor over an AF_UNIX socket and passes fds -- // with SCM_RIGHTS, neither of which the GPU policy allows. get/setsockopt -- // stay disallowed; add a narrow level/optname restriction if ever needed. -+ // with SCM_RIGHTS, neither of which the GPU policy allows. setsockopt -+ // stays disallowed; getsockopt is limited to SO_PEERCRED below. - #if defined(__NR_getpeername) - case __NR_getpeername: - #endif -@@ -49,6 +50,16 @@ ResultExpr XrProcessPolicy::EvaluateSyscall(int system_call_number) const { - case __NR_get_robust_list: - #endif - return Allow(); -+#if defined(__NR_getsockopt) -+ case __NR_getsockopt: { -+ // SteamVR's IPC client checks who is on the other end of its socket -+ // with SO_PEERCRED. Nothing else is readable. -+ const Arg level(1); -+ const Arg optname(2); -+ return If(AllOf(level == SOL_SOCKET, optname == SO_PEERCRED), Allow()) -+ .Else(Error(EPERM)); -+ } -+#endif - #if defined(__NR_kill) - case __NR_kill: { - // SteamVR probes its sibling processes for liveness with kill(pid, 0). -P -if ! printf '%s\n' "$PEERCRED_PATCH" | git apply --reverse --check 2>/dev/null; then - printf '%s\n' "$PEERCRED_PATCH" | git apply - stage "applied SO_PEERCRED patch" -fi -mkdir -p out/XR -cat > out/XR/args.gn <<'A' -target_os = "linux" -target_cpu = "arm64" -is_debug = false -is_official_build = false -is_component_build = false -dcheck_always_on = false -symbol_level = 0 -blink_symbol_level = 0 -v8_symbol_level = 0 -proprietary_codecs = true -ffmpeg_branding = "Chrome" -use_remoteexec = false -use_siso = true -treat_warnings_as_errors = false -A -stage "gn gen" -gn gen out/XR -gn args out/XR --list=enable_openxr --short | tee -a "$W/stage" -stage "build" -( while sleep 600; do guard || { pkill -u "$(id -u)" -f "siso|ninja"; exit 3; }; done ) & -GUARD=$! -trap 'kill $GUARD 2>/dev/null || true' EXIT -autoninja -C out/XR chrome chrome_sandbox chrome_crashpad_handler -stage "package" -cd out/XR -files=(chrome chrome_sandbox chrome_crashpad_handler *.pak *.bin icudtl.dat locales) -# GPU libraries aren't produced by every config; pack the ones that exist. -for f in libEGL.so libGLESv2.so libvk_swiftshader.so libvulkan.so.1 vk_swiftshader_icd.json; do - [ -e "$f" ] && files+=("$f") -done -tar -cJf "$W/chromium-xr-arm64.tar.xz" "${files[@]}" -stage "DONE $(ls -la $W/chromium-xr-arm64.tar.xz)" diff --git a/scripts/chromium-xr.sh b/scripts/chromium-xr.sh deleted file mode 100755 index f1a6be2..0000000 --- a/scripts/chromium-xr.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env zsh -# Mac-side: install and launch the WebXR-enabled Chromium build on the Frame. -# -# Flathub Chromium can't enter immersive WebXR on Linux: upstream only wires -# the OpenXR device on Windows (see docs/webxr-chromium.md). This deploys an -# arm64 build with the Linux OpenXR CLs, made on a Linux host by -# scripts/build-chromium-xr.sh, into ~/chromium-xr on the Frame (not a -# Flatpak, so SteamVR's sockets and the XR sandbox work unmodified). -# -# Usage: -# scripts/chromium-xr.sh install [TARBALL] # default: scp from $BUILD_HOST -# scripts/chromium-xr.sh launch [URL] # opens as its own panel in the headset -# scripts/chromium-xr.sh steam # adds "Chromium XR" to the Steam library -# scripts/chromium-xr.sh check # isSessionSupported via DevTools -set -euo pipefail - -FRAME_ALIAS=${FRAME_ALIAS:-frame} -BUILD_HOST=${BUILD_HOST:-} -BUILD_TARBALL=${BUILD_TARBALL:-chromium-xr/chromium-xr-arm64.tar.xz} -DEVTOOLS_PORT=${DEVTOOLS_PORT:-9223} -STEAM_NAME=${STEAM_NAME:-Chromium XR} -here=${0:A:h} -wrapper='~/Applications/ChromiumXR/launch.sh' - -# frame/chromium-xr/launch.sh holds Chrome's flags; both launch paths run it. -push_wrapper() { - # Write then rename, so a dropped connection can't leave a torn script. - ssh "$FRAME_ALIAS" 'mkdir -p ~/Applications/ChromiumXR && cd ~/Applications/ChromiumXR && cat > launch.sh.new && chmod +x launch.sh.new && mv launch.sh.new launch.sh' \ - < "$here/../frame/chromium-xr/launch.sh" -} - -case "${1:-}" in - install) - tarball=${2:-} - if [[ -z "$tarball" ]]; then - [[ -n "$BUILD_HOST" ]] || { print -u2 "Pass a tarball, or set BUILD_HOST to the build machine"; exit 2; } - tmp=$(mktemp -d) - trap 'rm -rf "$tmp"' EXIT - tarball=$tmp/chromium-xr-arm64.tar.xz - scp -q "$BUILD_HOST:$BUILD_TARBALL" "$tarball" - fi - ssh "$FRAME_ALIAS" 'rm -rf ~/chromium-xr.new && mkdir -p ~/chromium-xr.new' - ssh "$FRAME_ALIAS" 'tar -xJf - -C ~/chromium-xr.new' < "$tarball" - # Check the new build runs before replacing the old one. - ssh "$FRAME_ALIAS" '~/chromium-xr.new/chrome --version && rm -rf ~/chromium-xr && mv ~/chromium-xr.new ~/chromium-xr' - ;; - launch) - # Its own VR panel on gamescope's X display, so the Plasma desktop doesn't - # need to be open. DevTools is only on for this path (for `check`). - push_wrapper - exec "$here/panel-on-frame.sh" --name chromium-xr -- "$wrapper" \ - --remote-debugging-port="$DEVTOOLS_PORT" \ - "${2:-https://immersive-web.github.io/webxr-samples/}" - ;; - steam) - # A non-Steam shortcut, added through the Steam client's DevTools port - # without restarting Steam (see docs/apks.md). Launching it from the - # library gives Chromium its own panel like any game. Safe to rerun: it - # refreshes the wrapper and only adds the shortcut if it's missing. - ssh "$FRAME_ALIAS" 'test -x ~/chromium-xr/chrome' || - { print -u2 "No build in ~/chromium-xr on the Frame: run 'chromium-xr.sh install' first"; exit 1; } - push_wrapper - # The app id is kept next to the wrapper, so renaming the shortcut in the - # library doesn't make a rerun add a second one. - shortcuts=$here/../frame/android/steam_shortcuts.py - home=$(ssh "$FRAME_ALIAS" 'printf %s "$HOME"') - saved=$(ssh "$FRAME_ALIAS" 'cat ~/Applications/ChromiumXR/shortcut-appid 2>/dev/null || true') - existing=$(ssh "$FRAME_ALIAS" python3 - list < "$shortcuts" | - python3 -c 'import json,sys -apps = json.load(sys.stdin) -ids = [a["appid"] for a in apps if str(a["appid"]) == sys.argv[2]] or [a["appid"] for a in apps if a["name"] == sys.argv[1]] -print(ids[0] if ids else "")' "$STEAM_NAME" "$saved") - if [[ -n "$existing" ]]; then - print -r -- "The shortcut is already in the Steam library (app id $existing)" - else - icon='' - for dir in /var/lib/flatpak '~/.local/share/flatpak'; do - candidate=$dir/exports/share/icons/hicolor/256x256/apps/org.chromium.Chromium.png - if ssh "$FRAME_ALIAS" "test -f $candidate"; then icon=${candidate/#\~/$home}; break; fi - done - existing=$(ssh "$FRAME_ALIAS" python3 - add ${(q)STEAM_NAME} ${(q)home}/Applications/ChromiumXR/launch.sh ${(q)home} ${(q)icon} < "$shortcuts") - [[ "$existing" == <-> ]] || { print -u2 -r -- "Steam didn't return a shortcut app id: $existing"; exit 1; } - print -r -- "Added $STEAM_NAME to the Steam library (shortcut app id $existing)" - fi - ssh "$FRAME_ALIAS" "printf '%s\n' $existing > ~/Applications/ChromiumXR/shortcut-appid" - ;; - check) - # DevTools listens on the Frame's loopback only; evaluate there. - ssh "$FRAME_ALIAS" python3 - "$DEVTOOLS_PORT" <<'EOF' -import json, sys, urllib.request, base64, os, socket, struct -port = int(sys.argv[1]) -tabs = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json", timeout=10)) -page = next((t for t in tabs if t["type"] == "page"), None) -if page is None: - sys.exit("no open page: run 'chromium-xr.sh launch' first") -path = page["webSocketDebuggerUrl"].split(f":{port}", 1)[1] -s = socket.create_connection(("127.0.0.1", port), timeout=30) -key = base64.b64encode(os.urandom(16)).decode() -s.sendall(f"GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nUpgrade: websocket\r\n" - f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n" - "Sec-WebSocket-Version: 13\r\n\r\n".encode()) -s.recv(4096) -msg = json.dumps({"id": 1, "method": "Runtime.evaluate", "params": { - "expression": "navigator.xr ? navigator.xr.isSessionSupported('immersive-vr') : 'no navigator.xr'", - "awaitPromise": True}}).encode() -mask = os.urandom(4) -hdr = bytes([0x81]) + (bytes([0x80 | len(msg)]) if len(msg) < 126 - else bytes([0x80 | 126]) + struct.pack(">H", len(msg))) -s.sendall(hdr + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(msg))) -buf = b"" -reply = None -while reply is None: - chunk = s.recv(65536) - if not chunk: - sys.exit("DevTools closed the connection") - buf += chunk - # Consume every complete frame already buffered before reading again. - while len(buf) >= 2: - n = buf[1] & 0x7F - off = 2 - if n == 126: - if len(buf) < 4: - break - n, off = struct.unpack(">H", buf[2:4])[0], 4 - elif n == 127: - if len(buf) < 10: - break - n, off = struct.unpack(">Q", buf[2:10])[0], 10 - if len(buf) < off + n: - break - frame, buf = buf[off:off + n], buf[off + n:] - msg = json.loads(frame) - if msg.get("id") == 1: - reply = msg - break -print("immersive-vr supported:", reply["result"]["result"].get("value")) -EOF - ;; - *) sed -n '2,14p' "$0"; exit 2 ;; -esac