mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 08:00:09 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06f2dd63c1 |
No files matched your search
@@ -0,0 +1,59 @@
|
|||||||
|
# Install with FrameDrop (proof of concept)
|
||||||
|
|
||||||
|
[FrameDrop](https://framedropvr.com) is a Windows app that sideloads onto a Steam Frame: it copies a build to the headset and adds it to the Steam library. Issue #25 asks for an "Install with FrameDrop" button. Frametop isn't an app FrameDrop can copy over as is: it builds in a container, installs user services and a SteamVR driver, and asks questions in a terminal. So FrameDrop installs a small installer instead. Playing "Frametop" from the library runs the one-line installer (`get.sh --yes`) and shows its progress.
|
||||||
|
|
||||||
|
Nothing here is published yet: no release asset, no manifest on Pages, no button.
|
||||||
|
|
||||||
|
## How FrameDrop installs a Linux zip
|
||||||
|
|
||||||
|
It uses Valve's SteamOS Devkit path: pair once with the headset's devkit service, then rsync the unpacked zip into `~/devkit-game/<name>` over SSH, and register it with Steam as a Devkit Game with a start command. `devkit.sh` here makes the same calls with Valve's devkit-utils, so all of this can be tested on the Frame without a PC.
|
||||||
|
|
||||||
|
## What the probe found (SteamOS 0.3.0, build 20260922.6101926)
|
||||||
|
|
||||||
|
`probe/probe.sh`, started as a Devkit Game, recorded:
|
||||||
|
|
||||||
|
- Devkit titles run on the host, not in a container, as user `steamos`, from a process tree that Steam's reaper owns. This held even with the compat tool set to `SteamLinuxRuntime_4-arm64`: Steam recorded the mapping and still ran it on the host.
|
||||||
|
- On the host, everything the installer needs works: git, curl to GitHub, podman (sees the `dev` container), `systemctl --user`, `systemd-run --user`, and GTK 4 with libadwaita.
|
||||||
|
- A GTK window opens in gamescope (an X11 window on `:1`, drawn through gamescope's Vulkan WSI).
|
||||||
|
- Steam puts its overlay in `LD_PRELOAD` and Steam runtime paths in `LD_LIBRARY_PATH` and `PATH`. Every host tool prints a preload error unless they're cleared.
|
||||||
|
- Inside the Steam Linux Runtime 4 container (started by hand with its `run` script), there's no git, podman, systemctl, or GTK, but `flatpak-spawn --host` runs commands on the host.
|
||||||
|
- Steam refuses Devkit Game names with a `-` ("missing/invalid arguments").
|
||||||
|
- Steam doesn't make the start command executable: without the exec bit, the title exits in a second and nothing runs.
|
||||||
|
|
||||||
|
## The installer
|
||||||
|
|
||||||
|
`installer/frametop-install.sh` is the start command.
|
||||||
|
|
||||||
|
1. In the container, it starts itself again on the host with `flatpak-spawn --host`.
|
||||||
|
2. It clears Steam's preload and library paths.
|
||||||
|
3. It runs `curl get.sh | bash -s -- --yes` in a transient user service, `frametop-framedrop-install`, unless one is already running. The service is used because Steam ends the title's whole process tree when it's quit, and starts it with an OOM score of 900.
|
||||||
|
4. `installer/progress.py` (GTK 4 and libadwaita) follows the service's log, shows `install.sh`'s steps as a progress bar, and reports the result. Closing it leaves the install running. Playing the title again reattaches.
|
||||||
|
|
||||||
|
`--yes` keeps the version that's installed, or installs stable. It skips the parts that need sudo (our eye tracker and the Bluetooth fixes) and the SteamVR restart. The window says to restart SteamVR.
|
||||||
|
|
||||||
|
`--dry-run` clones into `~/.cache/frametop-framedrop/dry-run` and stops there (`get.sh --clone-only`).
|
||||||
|
|
||||||
|
## Try it on the Frame
|
||||||
|
|
||||||
|
```
|
||||||
|
framedrop/devkit.sh add FrametopTest framedrop/installer "./frametop-install.sh --dry-run"
|
||||||
|
framedrop/devkit.sh run FrametopTest # or Play it from the Steam library
|
||||||
|
framedrop/devkit.sh remove FrametopTest
|
||||||
|
framedrop/devkit.sh add FrametopProbe framedrop/probe "./probe.sh native" # the probe
|
||||||
|
```
|
||||||
|
|
||||||
|
The probe writes `~/.cache/frametop-framedrop/probe-native.log`, and the installer writes `~/.cache/frametop-framedrop/install.log`.
|
||||||
|
|
||||||
|
## Build the download
|
||||||
|
|
||||||
|
```
|
||||||
|
framedrop/build.sh [ZIP_URL]
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes `framedrop/build/Frametop.zip` (reproducible) and `frametop.framedrop.json`, FrameDrop's manifest with the zip's sha256. By default, `ZIP_URL` points at a `framedrop-installer` release asset. The button's link would be `https://framedropvr.com/install?manifest=https://deejanuz.github.io/frametop/frametop.framedrop.json`, with the manifest committed to main for Pages.
|
||||||
|
|
||||||
|
## Open questions, for a test with FrameDrop on a Windows PC
|
||||||
|
|
||||||
|
- Does FrameDrop keep or set the exec bit on `frametop-install.sh`? A zip unpacked on Windows loses it, and without it nothing runs.
|
||||||
|
- What start command does FrameDrop pick for this zip, and which runtime?
|
||||||
|
- Does the manifest's `name` become the Devkit Game name? It has to stay free of `-`.
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the FrameDrop download: framedrop/build/Frametop.zip (the installer in a "Frametop"
|
||||||
|
# folder) and framedrop/build/frametop.framedrop.json, the manifest an "Install with
|
||||||
|
# FrameDrop" button points at. The zip is reproducible: same files, same sha256.
|
||||||
|
#
|
||||||
|
# Usage: framedrop/build.sh [ZIP_URL]
|
||||||
|
# ZIP_URL where the zip will be downloaded from (default: the framedrop-installer release)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
url=${1:-https://github.com/DeeJanuz/frametop/releases/download/framedrop-installer/Frametop.zip}
|
||||||
|
out=$here/build
|
||||||
|
mkdir -p "$out"
|
||||||
|
|
||||||
|
python3 - "$here/installer" "$out/Frametop.zip" <<'EOF'
|
||||||
|
import os, sys, zipfile
|
||||||
|
src, dest = sys.argv[1:]
|
||||||
|
with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
|
for name in ("frametop-install.sh", "progress.py"):
|
||||||
|
info = zipfile.ZipInfo(f"Frametop/{name}", date_time=(2026, 1, 1, 0, 0, 0))
|
||||||
|
info.create_system = 3 # unix, so the permissions below count
|
||||||
|
info.external_attr = (0o100755 if os.access(os.path.join(src, name), os.X_OK) else 0o100644) << 16
|
||||||
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
with open(os.path.join(src, name), "rb") as f:
|
||||||
|
z.writestr(info, f.read())
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sha=$(sha256sum "$out/Frametop.zip" | cut -d' ' -f1)
|
||||||
|
python3 - "$url" "$sha" >"$out/frametop.framedrop.json" <<'EOF'
|
||||||
|
import json, sys
|
||||||
|
url, sha = sys.argv[1:]
|
||||||
|
print(json.dumps({
|
||||||
|
"schema": "framedrop.install/v1",
|
||||||
|
"name": "Frametop",
|
||||||
|
"files": [{"url": url, "sha256": sha}],
|
||||||
|
}, indent=2))
|
||||||
|
EOF
|
||||||
|
echo "built $out/Frametop.zip ($sha)"
|
||||||
|
echo " $out/frametop.framedrop.json"
|
||||||
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Do on the Frame what FrameDrop does from a PC: copy a folder into ~/devkit-game/NAME and
|
||||||
|
# register it with Steam as a "Devkit Game" (Valve's devkit-utils, the same calls FrameDrop and
|
||||||
|
# the SteamOS Devkit Client make over SSH). For testing the FrameDrop installer and the probe
|
||||||
|
# without a PC. Steam must be running, and Developer Mode on.
|
||||||
|
#
|
||||||
|
# Usage: devkit.sh add NAME DIR COMMAND [--compat TOOL]
|
||||||
|
# devkit.sh run NAME # start it, as the library's Play button does
|
||||||
|
# devkit.sh remove NAME # delete the folder and the Steam entry
|
||||||
|
# devkit.sh list
|
||||||
|
#
|
||||||
|
# NAME can't contain "-": Steam answers "missing/invalid arguments". COMMAND is relative to
|
||||||
|
# the folder, like FrameDrop's start command ("./probe.sh native"). --compat sets the runtime
|
||||||
|
# (SteamLinuxRuntime_4-arm64, say); on SteamOS 0.3.0 Steam recorded it but still ran the
|
||||||
|
# title on the host. There's no environment option: this Steam ignores the devkit env file.
|
||||||
|
#
|
||||||
|
# devkit-utils is Valve's (LGPL, gitlab.steamos.cloud/devkit/steamos-devkit). FrameDrop and
|
||||||
|
# the devkit client copy it to ~/devkit-utils; if that isn't there, it's fetched at a pinned
|
||||||
|
# commit into ~/.cache/frametop-framedrop.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
devkit_rev=a00ceb7d91ea44a0c3e714a91a06417d6e5cdb33
|
||||||
|
cache=$HOME/.cache/frametop-framedrop
|
||||||
|
|
||||||
|
usage() { sed -n '7,10p' "$0" | sed 's/^# \{0,1\}//' >&2; exit 2; }
|
||||||
|
|
||||||
|
utils() {
|
||||||
|
if [ -e "$HOME/devkit-utils/steam-client-create-shortcut" ]; then
|
||||||
|
echo "$HOME/devkit-utils"; return
|
||||||
|
fi
|
||||||
|
if [ ! -e "$cache/devkit-utils/steam-client-create-shortcut" ]; then
|
||||||
|
echo "fetching devkit-utils ($devkit_rev)" >&2
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
git -C "$tmp" init -q
|
||||||
|
git -C "$tmp" fetch -q --depth 1 https://gitlab.steamos.cloud/devkit/steamos-devkit.git "$devkit_rev"
|
||||||
|
git -C "$tmp" checkout -q FETCH_HEAD
|
||||||
|
mkdir -p "$cache"
|
||||||
|
rm -rf "$cache/devkit-utils"
|
||||||
|
cp -r "$tmp/client/devkit-utils" "$cache/devkit-utils"
|
||||||
|
rm -rf "$tmp"
|
||||||
|
fi
|
||||||
|
echo "$cache/devkit-utils"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ $# -ge 1 ] || usage
|
||||||
|
cmd=$1; shift
|
||||||
|
case $cmd in
|
||||||
|
add)
|
||||||
|
[ $# -ge 3 ] || usage
|
||||||
|
name=$1 src=$2 start=$3; shift 3
|
||||||
|
case $name in *-*) echo "NAME can't contain '-' (Steam refuses it)" >&2; exit 2 ;; esac
|
||||||
|
compat=
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case $1 in
|
||||||
|
--compat) compat=${2:?}; shift ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
u=$(utils)
|
||||||
|
dir=$(python3 "$u/steamos-prepare-upload" --gameid "$name" | python3 -c 'import json,sys; print(json.load(sys.stdin)["directory"])')
|
||||||
|
# FrameDrop rsyncs the unpacked zip; --delete as a clean upload would.
|
||||||
|
rsync -a --delete "$src/" "$dir/"
|
||||||
|
parms=$(python3 - "$name" "$dir" "$start" "$compat" <<'EOF'
|
||||||
|
import json, sys
|
||||||
|
name, directory, start, compat = sys.argv[1:]
|
||||||
|
print(json.dumps({
|
||||||
|
"gameid": name, "directory": directory,
|
||||||
|
"argv": [start], "env": {},
|
||||||
|
"settings": {"steam_play": "0", "compat_tool": compat},
|
||||||
|
"force_appid": "", "lepton_args": "",
|
||||||
|
}))
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
res=$(python3 "$u/steam-client-create-shortcut" --parms "$parms" | tail -1)
|
||||||
|
echo "Steam: $res"
|
||||||
|
case $res in *'"error"'*) exit 1 ;; esac
|
||||||
|
echo "registered $name ($dir)"
|
||||||
|
;;
|
||||||
|
run)
|
||||||
|
[ $# -eq 1 ] || usage
|
||||||
|
python3 "$(utils)/steam-devkit-rpc" run-game "gameid=$1" | tail -1
|
||||||
|
echo
|
||||||
|
;;
|
||||||
|
remove)
|
||||||
|
[ $# -eq 1 ] || usage
|
||||||
|
python3 "$(utils)/steamos-delete" --delete-title "$1"
|
||||||
|
rm -f "$HOME/devkit-game/$1"-*.json
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
python3 "$(utils)/steam-devkit-rpc" list-shortcuts
|
||||||
|
;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Frametop's FrameDrop installer: the start command of the "Frametop" title that FrameDrop
|
||||||
|
# puts in the Steam library. Playing it installs Frametop (or updates it) with the one-line
|
||||||
|
# installer, get.sh --yes, and shows its progress in a window.
|
||||||
|
#
|
||||||
|
# Steam ends a title's whole process tree when it's quit, and starts it with a high OOM score,
|
||||||
|
# so the install runs in a user service of its own (frametop-framedrop-install) and the window
|
||||||
|
# only follows its log. Quitting or closing the window leaves the install running; playing the
|
||||||
|
# title again reattaches to it.
|
||||||
|
#
|
||||||
|
# Usage: frametop-install.sh [--dry-run]
|
||||||
|
# --dry-run clone into ~/.cache/frametop-framedrop/dry-run instead of ~/frametop, and
|
||||||
|
# don't run install.sh (get.sh --clone-only), for testing this flow
|
||||||
|
set -u
|
||||||
|
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
self=$here/$(basename "$0")
|
||||||
|
|
||||||
|
# A Linux title can be started in the Steam Linux Runtime container, which has no git, podman,
|
||||||
|
# systemctl, or GTK. Start this again on the host. (SteamOS 0.3.0 runs devkit titles on the
|
||||||
|
# host anyway; this is for when FrameDrop or Steam picks the runtime.)
|
||||||
|
if [ -e /run/pressure-vessel ]; then
|
||||||
|
exec flatpak-spawn --host --watch-bus --env=DISPLAY="${DISPLAY-}" \
|
||||||
|
--env=XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR-}" --env=SteamAppId="${SteamAppId-}" \
|
||||||
|
--env=ENABLE_GAMESCOPE_WSI="${ENABLE_GAMESCOPE_WSI-}" /usr/bin/bash "$self" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Steam adds its overlay and runtime libraries to every child; host tools don't want them.
|
||||||
|
unset LD_PRELOAD LD_LIBRARY_PATH
|
||||||
|
export PATH=/usr/local/bin:/usr/bin:/bin
|
||||||
|
|
||||||
|
dry_run=0
|
||||||
|
[ "${1-}" = --dry-run ] && dry_run=1
|
||||||
|
|
||||||
|
unit=frametop-framedrop-install
|
||||||
|
state=$HOME/.cache/frametop-framedrop
|
||||||
|
log=$state/install.log
|
||||||
|
mkdir -p "$state"
|
||||||
|
|
||||||
|
get_args="--yes"
|
||||||
|
[ "$dry_run" = 1 ] && get_args="--yes --clone-only --dir $state/dry-run"
|
||||||
|
|
||||||
|
# Start the install unless one is running already. RemainAfterExit keeps its result for the
|
||||||
|
# window; the next start clears it.
|
||||||
|
if [ "$(systemctl --user show -P ActiveState "$unit" 2>/dev/null)" != activating ]; then
|
||||||
|
systemctl --user stop "$unit" 2>/dev/null
|
||||||
|
systemctl --user reset-failed "$unit" 2>/dev/null
|
||||||
|
: >"$log"
|
||||||
|
systemd-run --user --unit="$unit" --description="Frametop install (FrameDrop)" \
|
||||||
|
--property=Type=oneshot --property=RemainAfterExit=yes \
|
||||||
|
--property=StandardOutput="truncate:$log" --property=StandardError=inherit \
|
||||||
|
--setenv=HOME="$HOME" --setenv=PATH="$PATH" --setenv=TERM=dumb \
|
||||||
|
--working-directory="$HOME" --quiet --no-block \
|
||||||
|
/usr/bin/bash -c 'set -o pipefail; curl -fsSL https://deejanuz.github.io/frametop/get.sh | bash -s -- '"$get_args" ||
|
||||||
|
{ echo "couldn't start the install service" >&2; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec /usr/bin/python3 "$here/progress.py" "$unit" "$log"
|
||||||
Executable
+125
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Progress window for the FrameDrop installer: follows the install service's log and state.
|
||||||
|
|
||||||
|
Usage: progress.py UNIT LOG
|
||||||
|
|
||||||
|
Closing the window doesn't stop the install; it keeps running in the user service UNIT.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "4.0")
|
||||||
|
gi.require_version("Adw", "1")
|
||||||
|
from gi.repository import Adw, GLib, Gtk, Pango # noqa: E402
|
||||||
|
|
||||||
|
UNIT, LOG = sys.argv[1], sys.argv[2]
|
||||||
|
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
||||||
|
# install.sh marks each step with "== N/10 what it is"
|
||||||
|
STEP = re.compile(r"^== (\d+)/(\d+) (.*)$")
|
||||||
|
DONE_TEXT = ("Frametop is installed. Restart SteamVR once, or reboot the headset, so it loads "
|
||||||
|
"Frametop's driver. Then open Launch a program, then Desktop.")
|
||||||
|
|
||||||
|
|
||||||
|
def unit_state():
|
||||||
|
out = subprocess.run(
|
||||||
|
["systemctl", "--user", "show", "-P", "ActiveState", UNIT],
|
||||||
|
capture_output=True, text=True).stdout.strip()
|
||||||
|
return out or "inactive"
|
||||||
|
|
||||||
|
|
||||||
|
class Window(Adw.ApplicationWindow):
|
||||||
|
def __init__(self, app):
|
||||||
|
super().__init__(application=app, title="Frametop")
|
||||||
|
self.set_default_size(900, 600)
|
||||||
|
self.offset = 0
|
||||||
|
self.partial = ""
|
||||||
|
|
||||||
|
self.status = Gtk.Label(label="Installing Frametop", xalign=0, wrap=True)
|
||||||
|
self.status.add_css_class("title-2")
|
||||||
|
self.detail = Gtk.Label(label="Starting...", xalign=0, wrap=True)
|
||||||
|
self.bar = Gtk.ProgressBar()
|
||||||
|
self.bar.pulse()
|
||||||
|
|
||||||
|
self.text = Gtk.TextView(editable=False, cursor_visible=False, monospace=True,
|
||||||
|
wrap_mode=Pango.WrapMode.WORD_CHAR)
|
||||||
|
self.text.set_left_margin(8)
|
||||||
|
self.text.set_right_margin(8)
|
||||||
|
scroll = Gtk.ScrolledWindow(vexpand=True, child=self.text)
|
||||||
|
self.scroll = scroll
|
||||||
|
|
||||||
|
self.note = note = Gtk.Label(
|
||||||
|
label="You can close this window: the install keeps going. Play Frametop again to "
|
||||||
|
"come back to it.", xalign=0, wrap=True)
|
||||||
|
note.add_css_class("dim-label")
|
||||||
|
close = Gtk.Button(label="Close", halign=Gtk.Align.END)
|
||||||
|
close.connect("clicked", lambda *_: self.close())
|
||||||
|
|
||||||
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12,
|
||||||
|
margin_top=18, margin_bottom=18, margin_start=18, margin_end=18)
|
||||||
|
for w in (self.status, self.detail, self.bar, scroll, note, close):
|
||||||
|
box.append(w)
|
||||||
|
view = Adw.ToolbarView(content=box)
|
||||||
|
view.add_top_bar(Adw.HeaderBar())
|
||||||
|
self.set_content(view)
|
||||||
|
|
||||||
|
self.finished = False
|
||||||
|
GLib.timeout_add(500, self.tick)
|
||||||
|
self.tick()
|
||||||
|
|
||||||
|
def add_lines(self, lines):
|
||||||
|
buf = self.text.get_buffer()
|
||||||
|
for line in lines:
|
||||||
|
m = STEP.match(line)
|
||||||
|
if m:
|
||||||
|
n, total, what = int(m[1]), int(m[2]), m[3]
|
||||||
|
self.bar.set_fraction((n - 1) / total)
|
||||||
|
self.detail.set_label(f"Step {n} of {total}: {what}")
|
||||||
|
buf.insert(buf.get_end_iter(), line + "\n")
|
||||||
|
# Keep the view at the newest line.
|
||||||
|
end = buf.create_mark(None, buf.get_end_iter(), False)
|
||||||
|
self.text.scroll_mark_onscreen(end)
|
||||||
|
buf.delete_mark(end)
|
||||||
|
|
||||||
|
def tick(self):
|
||||||
|
try:
|
||||||
|
with open(LOG, "rb") as f:
|
||||||
|
f.seek(self.offset)
|
||||||
|
data = f.read()
|
||||||
|
self.offset += len(data)
|
||||||
|
except FileNotFoundError:
|
||||||
|
data = b""
|
||||||
|
if data:
|
||||||
|
text = self.partial + ANSI.sub("", data.decode("utf-8", "replace")).replace("\r", "\n")
|
||||||
|
*lines, self.partial = text.split("\n")
|
||||||
|
self.add_lines(lines)
|
||||||
|
|
||||||
|
state = unit_state()
|
||||||
|
if state == "activating":
|
||||||
|
if self.bar.get_fraction() == 0:
|
||||||
|
self.bar.pulse()
|
||||||
|
return True
|
||||||
|
if self.partial:
|
||||||
|
self.add_lines([self.partial])
|
||||||
|
self.partial = ""
|
||||||
|
self.note.set_visible(False)
|
||||||
|
if state == "active":
|
||||||
|
self.status.set_label("Done")
|
||||||
|
self.detail.set_label(DONE_TEXT)
|
||||||
|
self.bar.set_fraction(1)
|
||||||
|
else:
|
||||||
|
self.status.set_label("The install stopped")
|
||||||
|
self.detail.set_label("The log above says why. Play Frametop again to try again.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = Adw.Application(application_id="io.github.deejanuz.FrametopInstall")
|
||||||
|
app.connect("activate", lambda a: Window(a).present())
|
||||||
|
app.run([])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+114
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# FrameDrop proof of concept, step 1: run as a Steam "Devkit Game" (what FrameDrop installs a
|
||||||
|
# Linux zip as) and record what an installer started from Steam could use: host or Steam Linux
|
||||||
|
# Runtime container, podman, the user's systemd, git, the network, GTK, and a window on screen.
|
||||||
|
# Register and start it: framedrop/devkit.sh add FrametopProbe framedrop/probe "./probe.sh native"
|
||||||
|
# framedrop/devkit.sh run FrametopProbe
|
||||||
|
# Usage: probe.sh LABEL
|
||||||
|
# Writes ~/.cache/frametop-framedrop/probe-LABEL.log, replacing the previous one.
|
||||||
|
label=${1:-unlabeled}
|
||||||
|
out=$HOME/.cache/frametop-framedrop
|
||||||
|
mkdir -p "$out"
|
||||||
|
log=$out/probe-$label.log
|
||||||
|
exec >"$log" 2>&1
|
||||||
|
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
section() { printf '\n== %s\n' "$1"; }
|
||||||
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
check() { # check NAME CMD...: one line, ok or failed with the exit status
|
||||||
|
local name=$1; shift
|
||||||
|
if out_=$(timeout 20 "$@" 2>&1); then echo "ok $name: $(echo "$out_" | head -1)"
|
||||||
|
else echo "FAILED $name (exit $?): $(echo "$out_" | head -2 | tr '\n' ' ')"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
section "when, who, where"
|
||||||
|
date -Is
|
||||||
|
echo "label=$label uid=$(id -u) user=$(id -un) pwd=$PWD here=$here"
|
||||||
|
echo "args: $*"
|
||||||
|
grep -E '^(ID|VARIANT_ID|VERSION_ID|BUILD_ID|PRETTY_NAME)=' /etc/os-release
|
||||||
|
|
||||||
|
section "container?"
|
||||||
|
for p in /run/pressure-vessel /.flatpak-info /run/host/os-release; do
|
||||||
|
[ -e "$p" ] && echo "present: $p" || echo "absent: $p"
|
||||||
|
done
|
||||||
|
echo "container=${container-} PRESSURE_VESSEL_RUNTIME=${PRESSURE_VESSEL_RUNTIME-}"
|
||||||
|
[ -e /run/host/os-release ] && grep -E '^(ID|VARIANT_ID)=' /run/host/os-release
|
||||||
|
|
||||||
|
section "environment"
|
||||||
|
env | grep -E '^(PATH|HOME|XDG_[A-Z_]+|DISPLAY|WAYLAND_DISPLAY|DBUS_SESSION_BUS_ADDRESS|SteamAppId|SteamGameId|STEAM_COMPAT_[A-Z_]+|LD_LIBRARY_PATH|LD_PRELOAD|SDL_[A-Z_]+|GDK_BACKEND|ENABLE_[A-Z_]+|PRESSURE_VESSEL_[A-Z_]+)=' | sort
|
||||||
|
echo "process tree:"
|
||||||
|
pid=$$
|
||||||
|
for _ in 1 2 3 4 5 6 7 8; do
|
||||||
|
[ "$pid" -le 1 ] 2>/dev/null && break
|
||||||
|
printf ' %s %s\n' "$pid" "$(tr '\0' ' ' </proc/"$pid"/cmdline 2>/dev/null | cut -c1-200)"
|
||||||
|
pid=$(awk '/^PPid:/{print $2}' /proc/"$pid"/status 2>/dev/null)
|
||||||
|
done
|
||||||
|
|
||||||
|
# Steam adds its overlay (LD_PRELOAD) and runtime libraries to every child. An installer has
|
||||||
|
# to drop them before running host tools, as the checks below do.
|
||||||
|
unset LD_PRELOAD
|
||||||
|
LD_LIBRARY_PATH=$(printf %s "${LD_LIBRARY_PATH-}" | tr ':' '\n' | grep -v -e '/Steam/' -e '^$' -e 'x86_64' -e 'i386' | paste -sd:)
|
||||||
|
[ -n "$LD_LIBRARY_PATH" ] && export LD_LIBRARY_PATH || unset LD_LIBRARY_PATH
|
||||||
|
PATH=$(printf %s "$PATH" | tr ':' '\n' | grep -v '/Steam/' | paste -sd:)
|
||||||
|
echo "cleaned: PATH=$PATH LD_LIBRARY_PATH=${LD_LIBRARY_PATH-}"
|
||||||
|
|
||||||
|
section "tools"
|
||||||
|
for t in bash git curl python3 podman distrobox systemctl systemd-run busctl gdbus flatpak-spawn \
|
||||||
|
steam-runtime-launch-client konsole zenity kdialog; do
|
||||||
|
printf '%-28s %s\n' "$t" "$(command -v "$t" || echo -)"
|
||||||
|
done
|
||||||
|
|
||||||
|
section "what an installer needs"
|
||||||
|
check "home writable" sh -c 'f=$HOME/.cache/frametop-framedrop/.w && : >"$f" && rm "$f" && echo yes'
|
||||||
|
check "~/frametop visible" sh -c 'ls -d "$HOME/frametop" && git -C "$HOME/frametop" log -1 --format=%h'
|
||||||
|
have git && check "git ls-remote github" git ls-remote --heads https://github.com/DeeJanuz/frametop.git experimental
|
||||||
|
have curl && check "curl get.sh" sh -c 'curl -fsSL https://deejanuz.github.io/frametop/get.sh | head -1'
|
||||||
|
have podman && check "podman ps" podman ps --format '{{.Names}}'
|
||||||
|
have distrobox && check "distrobox list" distrobox list
|
||||||
|
have systemctl && check "systemctl --user" systemctl --user is-system-running
|
||||||
|
have systemctl && check "user units visible" systemctl --user is-enabled frametop-input-relay.service
|
||||||
|
have python3 && check "python3 gi Gtk 4" python3 -c 'import gi; gi.require_version("Gtk","4.0"); gi.require_version("Adw","1"); from gi.repository import Gtk, Adw; print(Gtk.get_major_version(), Gtk.get_minor_version())'
|
||||||
|
|
||||||
|
section "escape to the host (needed if this is a container)"
|
||||||
|
# A transient user unit runs in the host's user manager, outside any container.
|
||||||
|
if have systemd-run; then
|
||||||
|
rm -f "$out/escape-$label"
|
||||||
|
check "systemd-run --user" systemd-run --user --wait --collect --quiet -- \
|
||||||
|
sh -c "grep -E '^(ID|VARIANT_ID)=' /etc/os-release > '$out/escape-$label'; command -v podman git >> '$out/escape-$label'"
|
||||||
|
[ -s "$out/escape-$label" ] && sed 's/^/ host says: /' "$out/escape-$label"
|
||||||
|
fi
|
||||||
|
if have busctl; then
|
||||||
|
check "busctl --user (systemd1)" busctl --user get-property org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager Version
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "window"
|
||||||
|
# A plain GTK 4 window for 20 s. Watch for it on the Frame; "window: mapped" means GTK showed it.
|
||||||
|
if have python3; then
|
||||||
|
timeout 40 python3 - <<'EOF'
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
import gi
|
||||||
|
gi.require_version("Gtk", "4.0")
|
||||||
|
from gi.repository import Gtk, GLib
|
||||||
|
except Exception as e:
|
||||||
|
print("window: no GTK 4:", e); sys.exit(0)
|
||||||
|
app = Gtk.Application(application_id="io.github.deejanuz.FrametopProbe")
|
||||||
|
def on_activate(app):
|
||||||
|
w = Gtk.ApplicationWindow(application=app, title="Frametop installer probe")
|
||||||
|
w.set_default_size(640, 240)
|
||||||
|
w.set_child(Gtk.Label(label="Frametop installer probe\n\nIf you can read this, a FrameDrop install can show progress.\nThis window closes in 20 seconds."))
|
||||||
|
w.connect("map", lambda *_: print("window: mapped", flush=True))
|
||||||
|
w.present()
|
||||||
|
GLib.timeout_add_seconds(20, app.quit)
|
||||||
|
app.connect("activate", on_activate)
|
||||||
|
print("window: display", Gtk.Widget.get_display(Gtk.Label()) if hasattr(Gtk.Widget, "get_display") else "?", flush=True)
|
||||||
|
app.run([])
|
||||||
|
print("window: closed")
|
||||||
|
EOF
|
||||||
|
echo "window exit: $?"
|
||||||
|
else
|
||||||
|
echo "window: no python3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "done"
|
||||||
|
date -Is
|
||||||
Reference in new issue
Block a user