diff --git a/.github/tools/bundle.sh b/.github/tools/bundle.sh index 1fddd13..5812723 100755 --- a/.github/tools/bundle.sh +++ b/.github/tools/bundle.sh @@ -31,6 +31,23 @@ version="v$(grep -oP '^VERSION=\K.*' "$entry"), $(git describe --always --dirty sed -e '1{/^#!/d}' -e '/^# Sourced by steamify.sh/d' "lib/$lib.sh" echo done + # patches/ embedded: replaces lib/common.sh's patch_file, which reads + # them from the checkout. + echo "# ----- patches/ -----" + echo "patch_file() {" + echo ' case "$1" in' + for f in patches/*; do + [[ "$f" == patches/README.md ]] && continue + grep -q '^STEAMIFY_PATCH_EOF$' "$f" && { echo "bundle: $f contains the heredoc delimiter" >&2; exit 1; } + echo " $(basename "$f")) cat << 'STEAMIFY_PATCH_EOF'" + cat "$f" + echo "STEAMIFY_PATCH_EOF" + echo " ;;" + done + echo ' *) return 1 ;;' + echo ' esac' + echo "}" + echo echo "# ----- steamify.sh -----" echo "main() {" # Everything after the source loop is the entry point's body. diff --git a/.github/workflows/bundle.yml b/.github/workflows/bundle.yml index f45e092..9700290 100644 --- a/.github/workflows/bundle.yml +++ b/.github/workflows/bundle.yml @@ -6,7 +6,7 @@ # curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh | bash # curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/download/latest/steamify.sh | bash # Each release also has the app (v2): steamify-app.tar.gz (ui/, steamify.sh, -# lib/, the icon) and its launcher, run with: +# lib/, patches/, the icon) and its launcher, run with: # curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify-app.sh | bash # A push to feat/v2-ui (the app, not released yet) updates the pre-release # v2-ui-preview instead, whose launcher gets that build: @@ -36,7 +36,7 @@ jobs: .github/tools/bundle.sh dist/steamify.sh # The app: sources, not the single-file build (its backend mode runs # steamify.sh from its own folder). - tar -czf dist/steamify-app.tar.gz steamify.sh lib ui assets/steam-gaming-settings.svg LICENSE.md + tar -czf dist/steamify-app.tar.gz steamify.sh lib patches ui assets/steam-gaming-settings.svg LICENSE.md cp ui/steamify-app.sh dist/steamify-app.sh - name: Check diff --git a/AGENTS.md b/AGENTS.md index 376e54f..63d2afd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,10 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine - `steamify.sh` - the only entry point. Pre-flight checks, sources `lib/*.sh`, runs the menu and applies the plan. No component logic here. +- `patches/` - kernel module sources and patches the scripts build or + apply, never inline in the shell code. Read them with `patch_file + ` (`lib/common.sh`, from the checkout); the bundle embeds every file + in `patches/` and overrides `patch_file`. Add new ones to its README. - `lib/*.sh` - one file per responsibility, each defining functions only (no top-level side effects besides constants). See the table in `TECHNICAL.md`. @@ -25,6 +29,13 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine `_available` is checked via `component_available` (e.g. `machine` only on Fremont). Turn-on order is `COMPONENTS` order, turn-off reverse; `gaming` must stay first. Dependencies live in `toggle_component`. +- **Feature versions.** Set `FEATURE_VERSION[]` (`lib/menu.sh`) to the new + `VERSION` whenever what `_enable` sets up changes (a new component gets + one too): installs recorded with an older version + are ticked and re-applied ("update" in the plan and the app). New default + sub-options are ticked for installs whose parent is on (`feature_new`). + Status still comes from the system; don't add ad-hoc `_repair` checks + for new changes. - **Reversibility.** Every per-user KDE setting a component changes goes through `kset ` (`lib/state.sh`), which records the old value once; `_disable` calls @@ -204,6 +215,22 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine kernel only pairs that with a named lookup ("Port C") when the CEC driver registered first, which never happens since amdgpu loads from the initramfs. Without it `/dev/cec0` exists but stays at `f.f.f.f`. +- Power-off fix (`poweroff`, `lib/fremont-poweroff.sh`, a default sub-option + of `machine`): recent kernels (7.2 and the 6.x/7.0/7.1 updates with the + backport) keep the firmware's S4/S5 wake bit on GPIO + pin 18, so the Steam Machine boots again right after powering off + (Valve's kernel clears it at probe, not for upstream). The DKMS module + `steamify-fremont-poweroff` (`patches/steamify-fremont-poweroff.c`) clears + it in a power-off-prepare handler, built for each kernel. It made the kernel pin + unnecessary: `kpin` is only available while on (`kpin_available`), and + `detect_components` unticks it, so a normal run removes an existing pin. + `hdmi` (it needed the pin) is retired the same way: `hdmi_status` is on + while anything of it is left (saved displays, hotplug unit, EDID files, + old kernel parameter), `hdmi_available` only then, it's always unticked, + and `hdmi_disable` removes all of it. CachyOS's + `linux-cachyos` is clang-built, `-bore` GCC-built: let DKMS pick the + compiler, never pass `LLVM=1`. Test shutdown on the real machine for every + new major kernel. - `Relogin=true` means a gamescope that fails to start is relaunched in a tight loop; keep that in mind when changing session handling. diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b39fa..f92b5fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,49 @@ All notable changes, per version and per commit. Versions follow `steamify.sh`. Versions before 0.7.0 were numbered afterwards, one per merged pull request. +## 2.2.0 - 2026-09-27 + +- **docs: Changelog entries per commit for 2.2.0** +- `32795bd` **feat(ui): Update badge on the progress screen** +- `3c8ca65` **fix(ui): Never write the sudo password to a file; Update label on the progress screen** + - The app gave `sudo` the password through a short-lived file; the helper + now reads it from a named pipe the app feeds from memory each time sudo + asks. The progress screen showed "undefined" for an update. +- `11a91ef` **feat: Update badge on components that get a newer version** + - The menu (terminal and app) shows "(update)" / an Update badge after + the name of a component a normal run will update. +- `163f790` **feat: Feature versions are Steamify versions; Steam Machine support is 2.2.0** + - Each component records the Steamify version of what it set up; when a + release changes a component, installs with an older version are ticked + and updated by a normal run ("update" in the plan and the app's + review). A setup from before counts as 2.1.0, so Steam Machine support + (2.2.0) is updated on machines set up with 2.1.0. New default + sub-options (like the power-off fix) are ticked where their parent is on. +- `e121b06` **fix: Remove whatever is left of HDMI refresh boost, pin or no pin** + - HDMI refresh boost needed the pinned kernel and is retired (newer + kernels do HDMI 2.1 themselves): whatever is left of it (saved + displays, also ones not connected, its hotplug script, the old kernel + parameter) is unticked and removed by the next run. +- `19ca966` **docs: Drop the kernel pin and HDMI refresh boost descriptions** +- `56fb908` **docs: Power-off fix texts cover every affected kernel, not just 7.2** +- `6df19eb` **feat: Power-off fix as its own sub-option, drop the kernel pin, feature versions** + - The power-off fix is a sub-option of Steam Machine support, ticked + along with it (opt-out). The kernel pin is no longer offered: an + existing pin is shown unticked, so the next run removes it and brings + back CachyOS's current kernel. +- `7694c97` **feat: Power-off fix for the Steam Machine; the kernel pin is optional** + - With recent kernels (newer 6.x, 7.0 and 7.1 updates, 7.2 and probably + later ones) the Steam Machine started again right after shutting down: + they keep the wake bit the firmware sets on GPIO pin 18, which Valve's + own kernel clears in a patch that won't reach CachyOS. A small module, + built with DKMS for each installed kernel, clears it right before + power-off. Tested with CachyOS 7.2.8: stays off, and without the module + it rebooted. + - Module sources and patches live in `patches/` instead of inside the + shell code; the single-file build and the app package include them. The + Steam Machine CEC driver change is now + `patches/cros-ec-cec-single-port.patch`. + ## 2.1.0 - 2026-09-26 - **feat(ui): Manage button for HDMI refresh boost** diff --git a/README.md b/README.md index 460fa37..eb08c43 100644 --- a/README.md +++ b/README.md @@ -43,20 +43,11 @@ What Steamify can do on a Valve Steam Machine: TV's remote, and the TV turns on and off with the Steam Machine ([details](TECHNICAL.md#hdmi-cec)). - **Steam Machine support** - the front **LED bar** works, Steam's - **hardware settings** (fan, performance) work, and the power - button puts it to sleep like a console. - - **Pin the kernel to 7.1.6-1** - on by default: with newer CachyOS kernels - the Steam Machine reboots instead of shutting down. Untick it once CachyOS - fixes that. The kernel comes from its own - [release](https://github.com/theupriser/steamify-cachyos/releases/tag/kernel-7.1.6-1), - checked against CachyOS's signature - ([details](TECHNICAL.md#kernel-pin-steam-machine)). - - **HDMI refresh boost** - only with the pinned kernel, never ticked by - default: gets the highest refresh rate your HDMI display runs at the - desktop resolution (e.g. 110 Hz instead of 60 on a 3440x1440 monitor). - Each step is shown and you confirm it; no answer within 15 s switches - back. For more (e.g. 175 Hz), use DisplayPort - ([details](TECHNICAL.md#hdmi-refresh-boost-steam-machine)). + **hardware settings** (fan, performance) work, and the power button puts + it to sleep like a console. + - **Power-off fix** - on by default: shutting down really turns the Steam + Machine off, also with the newest CachyOS kernel + ([details](TECHNICAL.md#power-off-fix-steam-machine)). - **Update BIOS** - installs Valve's newest Steam Machine BIOS. Never ticked by default, at your own risk, and only after two warnings ([details](TECHNICAL.md#bios-updates-steam-machine)). diff --git a/TECHNICAL.md b/TECHNICAL.md index 73d4285..2bb7b10 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -208,84 +208,54 @@ upset CEC for the TV's other devices Turning it off removes the packages again (`linuxconsole` only if the wizard installed it). -## Kernel pin (Steam Machine) +## Power-off fix (Steam Machine) -With CachyOS kernels newer than 7.1.6 a Steam Machine reboots instead of -shutting down. The **Pin the kernel** sub-option (ticked along with Steam -Machine support) installs `linux-cachyos` and `linux-cachyos-headers` -7.1.6-1 and adds them to `IgnorePkg` in `/etc/pacman.conf`, so updates skip -them. DKMS builds the LED driver for it; restart to boot it. +With recent kernels a Steam Machine starts again right after powering off: +the newer 6.x, 7.0 and 7.1 updates and 7.2 (and probably every kernel after +it). Linux stopped clearing the S4/S5 wake bits at probe +(`pinctrl-amd: Don't clear S4 wake bits at probe`, in 7.2 and backported to +stable kernels), and the firmware leaves +that bit set on GPIO pin 18 (`_SB.PCI0.GPP6`). Valve's own kernel +(`linux-neptune-72`) clears it at probe on Fremont, in a patch marked not +for upstream ("until the firmware is fixed"), so CachyOS and mainline won't +get it. -The packages (and their signatures, which pacman checks) are kept in -`/var/cache/steamify/kernel`, so re-applying needs no download. Missing -files are taken from pacman's cache, else downloaded from this repo's -`kernel-7.1.6-1` release, then `archive.cachyos.org`, then -`mirror.cachyos.org` (which only has the current kernel). Every file must -match the SHA-256 in the script and have a valid CachyOS signature; a bad -one is deleted, so the next run downloads it again. Set `PINNED_KERNEL_URL` -to a directory URL with the files to try another source first, or drop them -into the kernel directory yourself. +The **Power-off fix** sub-option of Steam Machine support (ticked along with +it, can be unticked) builds a small module with DKMS for every installed +kernel, `steamify-fremont-poweroff` (source in +[`patches/steamify-fremont-poweroff.c`](patches/steamify-fremont-poweroff.c)). +It only loads on Fremont (DMI board name) and touches one register: right +before power-off (a `SYS_OFF_MODE_POWER_OFF_PREPARE` handler, after the +drivers have shut down) it clears that pin's S4/S5 wake bit. On a kernel +that already clears it, it does nothing. It logs the pin's state at load: +`dmesg | grep 'GPIO 18'`; `/sys/kernel/debug/gpio` shows the S4/S5 column. +Tested on a Steam Machine with `linux-cachyos-bore` 7.2.8: it stayed off +three times in a row, and rebooted right away with the module unloaded. -Unticking it removes the pin and runs `sudo pacman -Syu`, which brings the -kernel back to CachyOS's current version; the files stay for next time. +Where Steam Machine support was set up before 2.2.0, the menu ticks the +power-off fix as a new default sub-option (see feature versions below), so a +normal run adds it. -## HDMI refresh boost (Steam Machine) +## Feature versions (updates) -With the pinned kernel (7.1.6), HDMI displays often stay at 60 Hz. Two -reasons: +Each component has a feature version (`FEATURE_VERSION` in `lib/menu.sh`): +the Steamify version in which what it sets up last changed (2.1.0 for +everything that hasn't changed since). After a component is turned on +successfully, that version is recorded in `~/.local/state/cachyos-gamescope-boot/features.state` +(turning it off records `off`). Whether a component is on is always checked +on the system itself; the version only decides about updates: -- Monitors list their fast modes in an extra EDID block, announced by the - HDMI Forum EEODB data block. 7.1.6 only reads the first extension block, - so it never sees them. Newer kernels do. -- Their fastest modes need HDMI 2.1 (FRL). 7.1.6's amdgpu only does HDMI - 2.0 (TMDS, at most 600 MHz), but a mode with the display's own shortest - blanking at a slightly lower rate often fits. +- **On, recorded version older** than the current one: ticked and + re-applied by a normal run; the plan (and the app's review) says + "update". A setup from before versions were recorded counts as 2.1.0, + so on a machine set up with 2.1.0, Steam Machine support (2.2.0) is + updated. +- **A new default sub-option** (not in `NO_PRESELECT`) whose parent is on + and that was never turned on or off: ticked, so a normal run adds it. + Turning it off once records `off`, so it isn't ticked again. -The menu item (only on a Steam Machine with the pinned kernel, never -ticked by default, run from the desktop in Konsole): - -1. Takes the desktop resolution from KDE (`kscreen-doctor -j`) and reads the - display's complete EDID over DDC (`i2ctransfer`, segment pointer 0x30). - A live EDID left by an earlier test is cleared first. -2. Calculates the highest rate that fits: the display's TMDS limit (HDMI - Forum VSDB, capped at amdgpu's 600 MHz), its shortest blanking at that - resolution and its maximum refresh (range limits, VRR maximum). Steps: - that rate rounded down to ten, and the hundred below it as a safe option. - Rates the display already lists, or that aren't faster than what works - now, are left out. -3. Builds the EDID: all of the display's blocks, the block count and EEODB - fixed, plus a DisplayID block with the steps. -4. Loads it live (debugfs `edid_override`, `trigger_hotplug`) and switches - to each step, lowest first. Each one needs a "y" within 15 s - (`WIZARD_HDMI_CONFIRM_SECONDS` for tests); anything else switches back - and stops. -5. Saves the confirmed steps for that display: the EDID as - `/usr/lib/firmware/edid/steamify-.bin`, where `` is the display's - manufacturer, model, serial and date (EDID bytes 8-17), and a line in - `/etc/steamify/hdmi-edid.conf` (id, name, mode, rates). Other saved - displays are kept. - -`steamify-edid.service` (at boot, before the login manager) and a udev rule -(`90-steamify-edid.rules`, every drm hotplug) run -`/usr/local/bin/steamify-edid-hotplug`. Per HDMI port it reads the connected -display's ID over DDC (the real display, even while an override is loaded) -and loads that display's saved EDID through debugfs, or resets the port to -the display's own EDID when there is none, or no display. What's loaded per -port is kept in `/run/steamify-edid`, so the hotplug the script triggers -itself doesn't loop. - -The item is on when the connected display runs on its saved EDID; with -another display it's off, and ticking it sets that one up. Turning it off -removes the connected display's EDID; the unit and rule go with the last -one. In the app the item is a **Set up…** button, and **Manage** once a -display is saved: it lists every saved display, removes any of them, and -sets up the connected display when it has none. Unpinning the kernel -removes them all. - -Versions before 2.1.0 used `drm.edid_firmware=` on the kernel command line -(and the initramfs), which applied to any display on that port; re-applying -saves such a setup per display and removes the parameter. Untick it before removing the -kernel pin: newer kernels read the EDID themselves and can do HDMI 2.1. +Set a component's entry to the new `VERSION` whenever what its +`_enable` sets up changes. ## BIOS updates (Steam Machine) @@ -355,9 +325,10 @@ immediately, which can turn into a loop - see | `lib/wizard-shortcut.sh` | Steamify shortcut: desktop icon and launcher entry that run the newest release | | `lib/bios.sh` | Update BIOS (Steam Machine, opt-in): current/newest version, double confirmation, fwupd | | `lib/cec.sh` | HDMI-CEC: Valve's `cecd` and friends from its `holo` repository | -| `lib/steam-machine.sh` | Steam Machine support: LED driver, LED access, steamos-manager; kernel pin | -| `lib/hdmi-refresh.sh` | HDMI refresh boost (Steam Machine, pinned kernel): EDID over DDC, calculated steps, live test, `drm.edid_firmware` | -| `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`) | +| `lib/steam-machine.sh` | Steam Machine support: LED driver, LED access, steamos-manager | +| `lib/fremont-poweroff.sh` | Steam Machine support: the power-off fix (DKMS module from `patches/`) | +| `patches/` | Module sources and patches the scripts build or apply (`patch_file`); see its README | +| `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`), with `patches/` embedded | | `.github/workflows/bundle.yml` | Builds and checks it on every push; publishes it on `main` | The single-file version is generated: on every push to `main`, GitHub diff --git a/lib/backend.sh b/lib/backend.sh index 776d91c..da6e867 100644 --- a/lib/backend.sh +++ b/lib/backend.sh @@ -62,6 +62,7 @@ backend_status() { items+=",\"kind\":\"$kind\",\"parent\":$(json_str "$parent")" items+=",\"on\":$( [[ "$now" == 1 ]] && echo true || echo false)" items+=",\"wanted\":$( [[ "${WANTED[$c]:-0}" == 1 ]] && echo true || echo false)" + items+=",\"update\":$(feature_outdated "$c" && echo true || echo false)" items+=",\"selectable\":$(component_selectable "$c" && echo true || echo false)}" done local cec="" f @@ -160,6 +161,8 @@ backend_apply() { # The same rules as the menu's toggles. [[ "${WANTED[single]:-0}" == 1 ]] && WANTED[gaming]=1 [[ "${WANTED[gaming]:-0}" == 0 ]] && { WANTED[single]=0; WANTED[boot]=0; } + [[ "${WANTED[poweroff]:-0}" == 1 ]] && WANTED[machine]=1 + [[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[poweroff]=0 [[ "${WANTED[kpin]:-0}" == 1 ]] && WANTED[machine]=1 [[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[kpin]=0 [[ "${WANTED[hdmi]:-0}" == 1 ]] && { WANTED[machine]=1; WANTED[kpin]=1; } @@ -179,8 +182,12 @@ backend_apply() { LOGIN_MANAGER=plasmalogin [[ "${WANTED[single]}" == 1 ]] && LOGIN_MANAGER=sddm local -a failed=() - for c in "${TO_DISABLE[@]}"; do backend_run_component "$c" disable || failed+=("$c"); done - for c in "${TO_ENABLE[@]}"; do backend_run_component "$c" enable || failed+=("$c"); done + for c in "${TO_DISABLE[@]}"; do + if backend_run_component "$c" disable; then feature_record "$c" disable; else failed+=("$c"); fi + done + for c in "${TO_ENABLE[@]}"; do + if backend_run_component "$c" enable; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi + done [[ " ${TO_DISABLE[*]} ${TO_ENABLE[*]} " =~ \ (gaming|single|boot|kpin)\ ]] && RESTART_FOR_LOGIN=true backend_event finished "\"failed\":$(json_list "${failed[@]}"),\"restart\":$(restart_needed && echo true || echo false)" } diff --git a/lib/cec.sh b/lib/cec.sh index d6f40c2..e8e4724 100644 --- a/lib/cec.sh +++ b/lib/cec.sh @@ -60,8 +60,8 @@ cec_reload_driver() { cec_driver_enable() { detect_valve_fremont || return 0 local tmp k - if ! pacman -Q dkms >/dev/null 2>&1; then - sudo pacman -S --needed --noconfirm dkms || { err "Installing dkms failed."; return 1; } + if ! pacman -Q dkms patch >/dev/null 2>&1; then + sudo pacman -S --needed --noconfirm dkms patch || { err "Installing dkms failed."; return 1; } fi install_kernel_headers || return 1 tmp="$(mktemp -d)" @@ -72,13 +72,8 @@ cec_driver_enable() { err "Downloading Valve's CEC driver failed (or its checksum didn't match)." return 1 fi - # amdgpu registers its HDMI notifier without a port name, which only - # matches the driver's named lookup ("Port C") if the driver loaded - # first; amdgpu loads from the initramfs, so it never does. With a - # single CEC port, look it up by device alone. - sed -i 's/cec_notifier_cec_adap_register(hdmi_dev, conns\[port_num\],/cec_notifier_cec_adap_register(hdmi_dev, conns[1] ? conns[port_num] : NULL,/' \ - "$tmp/cros-ec-cec.c" - grep -q 'conns\[1\] ? conns\[port_num\] : NULL' "$tmp/cros-ec-cec.c" || + # So it finds amdgpu's HDMI port (see the patch for why). + patch_file cros-ec-cec-single-port.patch | patch -s -d "$tmp" -p1 || { rm -rf "$tmp"; err "Patching Valve's CEC driver failed."; return 1; } echo 'obj-m += cros-ec-cec.o' >"$tmp/Makefile" printf '%s\n' "# Written by Steamify: Valve's cros_ec_cec, which knows the Steam Machine." \ diff --git a/lib/common.sh b/lib/common.sh index 4ad8495..dfcf60e 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -6,6 +6,9 @@ c_reset="\033[0m"; c_bold="\033[1m"; c_green="\033[32m"; c_yellow="\033[33m"; c_ info() { echo -e "${c_cyan}[INFO]${c_reset} $*"; } ok() { echo -e "${c_green}[OK]${c_reset} $*"; } +# patch_file : a file from patches/ (module sources, patches). The +# single-file build replaces this with one that has them embedded. +patch_file() { cat "$SCRIPT_DIR/patches/$1"; } warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; } err() { echo -e "${c_red}[ERROR] $*${c_reset}" >&2; } ask_yn() { diff --git a/lib/fremont-poweroff.sh b/lib/fremont-poweroff.sh new file mode 100644 index 0000000..2ea8482 --- /dev/null +++ b/lib/fremont-poweroff.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# "Power-off fix" menu item (poweroff), a sub-option of Steam Machine support +# ticked along with it (opt-out): the Steam Machine powers off instead of +# booting up again. Recent kernels (7.2, and the 6.x, 7.0 and 7.1 updates +# that got the change backported) keep the S4/S5 wake bit the firmware +# leaves set on GPIO pin 18 ("pinctrl-amd: Don't clear S4 wake bits at +# probe"), so the machine starts again right after powering off. Valve's kernel clears it at probe on Fremont, in +# a patch not meant for upstream, so CachyOS won't get it. A small module +# (patches/steamify-fremont-poweroff.c) clears it right before power-off; +# DKMS builds it for each installed kernel. On a kernel that clears it +# itself it does nothing. +# Sourced by steamify.sh; not meant to be run on its own. + +POWEROFF_DKMS_NAME=steamify-fremont-poweroff +POWEROFF_DKMS_VER=1 +POWEROFF_DKMS_SRC="/usr/src/$POWEROFF_DKMS_NAME-$POWEROFF_DKMS_VER" +POWEROFF_MODULES_LOAD=/etc/modules-load.d/steamify-fremont-poweroff.conf + +poweroff_fix_installed() { [[ -f "$POWEROFF_DKMS_SRC/dkms.conf" && -f "$POWEROFF_MODULES_LOAD" ]]; } + +poweroff_fix_enable() { + detect_valve_fremont || return 0 + local tmp k + if ! pacman -Q dkms >/dev/null 2>&1; then + sudo pacman -S --needed --noconfirm dkms || { err "Installing dkms failed."; return 1; } + fi + install_kernel_headers || return 1 + tmp="$(mktemp -d)" + patch_file "$POWEROFF_DKMS_NAME.c" > "$tmp/$POWEROFF_DKMS_NAME.c" + echo "obj-m += $POWEROFF_DKMS_NAME.o" >"$tmp/Makefile" + printf '%s\n' "# Written by Steamify: the Steam Machine powers off instead of booting up again." \ + "PACKAGE_NAME=\"$POWEROFF_DKMS_NAME\"" "PACKAGE_VERSION=\"$POWEROFF_DKMS_VER\"" \ + "BUILT_MODULE_NAME[0]=\"$POWEROFF_DKMS_NAME\"" 'DEST_MODULE_LOCATION[0]="/updates/dkms"' \ + 'AUTOINSTALL="yes"' >"$tmp/dkms.conf" + sudo dkms remove "$POWEROFF_DKMS_NAME/$POWEROFF_DKMS_VER" --all >/dev/null 2>&1 + sudo rm -rf "$POWEROFF_DKMS_SRC" + sudo install -d "$POWEROFF_DKMS_SRC" && + sudo install -m644 "$tmp"/{"$POWEROFF_DKMS_NAME.c",Makefile,dkms.conf} "$POWEROFF_DKMS_SRC/" + rm -rf "$tmp" + sudo dkms add "$POWEROFF_DKMS_NAME/$POWEROFF_DKMS_VER" >/dev/null || + { err "Adding the power-off fix to DKMS failed."; return 1; } + for k in /usr/lib/modules/*/build; do + k="$(basename "$(dirname "$k")")" + info "Building the power-off fix for $k..." + sudo dkms install "$POWEROFF_DKMS_NAME/$POWEROFF_DKMS_VER" -k "$k" >/dev/null || + warn "Building the power-off fix for $k failed; with that kernel it may start again after shutting down." + done + echo "$POWEROFF_DKMS_NAME" | sudo tee "$POWEROFF_MODULES_LOAD" >/dev/null + sudo modprobe "$POWEROFF_DKMS_NAME" 2>/dev/null + return 0 +} + +poweroff_fix_disable() { + sudo rm -f "$POWEROFF_MODULES_LOAD" + sudo modprobe -r "$POWEROFF_DKMS_NAME" 2>/dev/null + [[ -d "$POWEROFF_DKMS_SRC" ]] || return 0 + sudo dkms remove "$POWEROFF_DKMS_NAME/$POWEROFF_DKMS_VER" --all >/dev/null 2>&1 + sudo rm -rf "$POWEROFF_DKMS_SRC" +} + +poweroff_status() { poweroff_fix_installed; } +poweroff_enable() { + info "Installing the power-off fix (the machine stays off after shutting down)..." + poweroff_fix_enable || { err "Installing the power-off fix failed."; return 1; } + ok "Power-off fix on: the Steam Machine stays off after shutting down." +} +poweroff_disable() { + poweroff_fix_disable + ok "Power-off fix removed; with recent kernels the Steam Machine may start again after shutting down." +} diff --git a/lib/hdmi-refresh.sh b/lib/hdmi-refresh.sh index a2a8fd8..b3b8ea2 100644 --- a/lib/hdmi-refresh.sh +++ b/lib/hdmi-refresh.sh @@ -56,18 +56,17 @@ hdmi_cmdline_param() { } hdmi_available() { - # Stays available while on, so it can be turned off after the pin is gone. - detect_valve_fremont || return 1 - # Saved displays too, so they can be removed without the pin. - hdmi_status || [[ -n "$(hdmi_saved)" ]] || { pinned_kernel_installed && [[ -n "$(hdmi_connectors)" ]]; } + # No longer offered (it needed the pinned kernel): only shown while + # anything of it is left, so a normal run removes it. + detect_valve_fremont && hdmi_status } hdmi_status() { - # On = the connected display runs on its saved EDID. Another display on - # the port is off, so ticking it sets that one up. The command line: set - # up by an older version, re-applying moves it over. - [[ -n "$(hdmi_active_ids)" ]] || - { [[ -n "$(hdmi_cmdline_param)" ]] && compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null; } + # On while anything of it is left: saved displays (connected or not), the + # hotplug script, an EDID file, or the kernel parameter of versions + # before 2.1.0. + [[ -n "$(hdmi_saved)" || -f "$HDMI_UNIT" || -n "$(hdmi_cmdline_param)" ]] || + compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null } hdmi_saved() { [[ -f "$HDMI_MAP" ]] && grep -v '^#' "$HDMI_MAP"; } @@ -688,16 +687,10 @@ hdmi_enable() { } hdmi_disable() { - # For the connected display; other saved displays stay (the app lists - # them). Without the pinned kernel none may stay: newer kernels read the - # EDID themselves. - local id - if [[ -n "$(hdmi_cmdline_param)" ]]; then - hdmi_forget all; hdmi_remove_boot_param || return 1; state_clear hdmi - elif [[ "${WANTED[kpin]:-1}" == 0 ]] || ! pinned_kernel_installed; then - hdmi_forget all - else - for id in $(hdmi_active_ids); do hdmi_forget "$id"; done - fi - ok "HDMI refresh boost removed for the connected display; it uses its own EDID again." + # Everything, for every saved display: newer kernels read the whole EDID + # themselves, and an old one loaded at the next hotplug would be wrong. + hdmi_forget all + hdmi_remove_boot_param || return 1 + state_clear hdmi + ok "HDMI refresh boost removed; displays use their own EDID again." } diff --git a/lib/menu.sh b/lib/menu.sh index 9fe7e97..2d8baec 100644 --- a/lib/menu.sh +++ b/lib/menu.sh @@ -5,18 +5,30 @@ # Menu order. Components are turned on in this order and off in reverse; # gaming must come first (single user builds on it). -COMPONENTS=(gaming boot theme glyphs single launcher cec machine kpin hdmi bios) +COMPONENTS=(gaming boot theme glyphs single launcher cec machine poweroff kpin hdmi bios) # One-off actions rather than on/off components: never preselected, never # re-applied, not listed as on or off. ACTIONS=(bios) # Sub-options, shown indented under their parent and only while it's ticked. -declare -A PARENT=([boot]=gaming [kpin]=machine [hdmi]=machine [bios]=machine) +declare -A PARENT=([boot]=gaming [poweroff]=machine [kpin]=machine [hdmi]=machine [bios]=machine) # Never preselected on a first run: booting into the desktop is a choice, # gamescope is the default; HDMI-CEC is opt-in (it can wake the machine or # upset other devices on the TV, even on SteamOS), except on a Steam Machine, # which has CEC like on SteamOS. HDMI refresh boost needs someone at the # screen to confirm each step. -NO_PRESELECT=(boot cec hdmi) +NO_PRESELECT=(boot cec kpin hdmi) +# Feature versions: the Steamify version in which what a component's enable +# sets up last changed; set it to the new VERSION whenever you change one. +# Each successful run records it (state "features"); a component that's on +# with an older one is ticked and re-applied by a normal run, and the app +# shows it as an update. A setup from before these were recorded counts as +# FEATURE_BASELINE. +FEATURE_BASELINE=2.1.0 +declare -A FEATURE_VERSION=( + [gaming]=2.1.0 [boot]=2.1.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0 + [launcher]=2.1.0 [cec]=2.1.0 [machine]=2.2.0 [poweroff]=2.2.0 + [kpin]=2.1.0 [hdmi]=2.1.0 +) declare -A LABEL=( [gaming]="SteamOS conversion: boot into gaming mode, Steam on the desktop" @@ -27,7 +39,8 @@ declare -A LABEL=( [launcher]="Steamify shortcut: the app on the desktop, Steamify Terminal in the launcher" [cec]="HDMI-CEC: use Steam with the TV remote, TV on/off with the PC (experimental)" [machine]="Steam Machine support: LED bar driver, hardware settings in Steam" - [kpin]="Pin the kernel to $PINNED_KERNEL_VER (fixes rebooting after shutdown)" + [poweroff]="Power-off fix: the Steam Machine stays off after shutting down" + [kpin]="Pin the kernel to $PINNED_KERNEL_VER (untick for CachyOS's current kernel)" [hdmi]="HDMI refresh boost: highest refresh your HDMI display runs" [bios]="Update BIOS" ) @@ -35,7 +48,8 @@ declare -A CURRENT WANTED component_available() { case "$1" in - machine|kpin) machine_available ;; + machine|poweroff) machine_available ;; + kpin) kpin_available ;; hdmi) hdmi_available ;; bios) bios_available ;; esac @@ -56,6 +70,42 @@ menu_visible() { [[ -z "${PARENT[$1]:-}" || "${WANTED[${PARENT[$1]}]:-0}" == 1 ]] } +feature_record() { + # feature_record : after a successful run. + if [[ "$2" == enable ]]; then state_set features "$1" "${FEATURE_VERSION[$1]:-$FEATURE_BASELINE}" + else state_set features "$1" off; fi +} + +feature_outdated() { + # On, but set up by an older version of that feature (compared as + # versions: 2.10.0 is newer than 2.9.0). + local have want + [[ "${CURRENT[$1]:-0}" == 1 ]] || return 1 + have="$(state_get features "$1" "$FEATURE_BASELINE")" + [[ "$have" == off ]] && have="$FEATURE_BASELINE" + want="${FEATURE_VERSION[$1]:-$FEATURE_BASELINE}" + [[ "$have" != "$want" && "$(printf '%s\n' "$have" "$want" | sort -V | head -n 1)" == "$have" ]] +} + +menu_label() { + # The label, with "(update)" after the name when a newer version of it + # will be applied. + local l="${LABEL[$1]}" + if feature_outdated "$1"; then + if [[ "$l" == *:* ]]; then l="${l%%:*} ${c_yellow}(update)${c_reset}:${l#*:}" + else l+=" ${c_yellow}(update)${c_reset}"; fi + fi + printf '%s' "$l" +} + +feature_new() { + # A default sub-option added after its parent was set up (e.g. the + # power-off fix under Steam Machine support): never turned on or off. + [[ -n "${PARENT[$1]:-}" && "${CURRENT[${PARENT[$1]}]:-0}" == 1 && "${CURRENT[$1]:-0}" == 0 ]] && + ! is_action "$1" && [[ " ${NO_PRESELECT[*]} " != *" $1 "* ]] && + [[ -z "$(state_get features "$1")" ]] +} + component_selectable() { # Greyed out and not tickable when it has nothing to do. case "$1" in @@ -72,6 +122,17 @@ detect_components() { done # HDMI-CEC set up by an older version: tick it, so a normal run fixes it. component_available cec && cec_repair && WANTED[cec]=1 + # Updated features and new default sub-options: ticked, so a normal run + # applies them. + for c in "${COMPONENTS[@]}"; do + component_available "$c" || continue + { feature_outdated "$c" || feature_new "$c"; } && WANTED[$c]=1 + done + # The kernel pin is no longer needed (the power-off fix), and HDMI + # refresh boost needed the pin: whatever is left of either is unticked, + # so a normal run removes it. + component_available kpin && WANTED[kpin]=0 + component_available hdmi && WANTED[hdmi]=0 # The terminal-only Steamify shortcut from before 2.0.1: tick it, so a # normal run replaces it with the app. launcher_repair && WANTED[launcher]=1 @@ -96,8 +157,10 @@ toggle_component() { if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then WANTED[single]=0; WANTED[boot]=0; fi # Where to boot to is part of the conversion, too. if [[ "$c" == boot && "${WANTED[boot]}" == 1 ]]; then WANTED[gaming]=1; fi - # The kernel pin is opt-out: ticked along with Steam Machine support. - if [[ "$c" == machine ]]; then WANTED[kpin]=${WANTED[machine]}; fi + # The power-off fix is opt-out: ticked along with Steam Machine support. + if [[ "$c" == machine ]]; then WANTED[poweroff]=${WANTED[machine]}; fi + if [[ "$c" == poweroff && "${WANTED[poweroff]}" == 1 ]]; then WANTED[machine]=1; fi + if [[ "$c" == machine && "${WANTED[machine]}" == 0 ]]; then WANTED[kpin]=0; fi if [[ "$c" == kpin && "${WANTED[kpin]}" == 1 ]]; then WANTED[machine]=1; fi # HDMI refresh boost and the BIOS update sit under Steam Machine support; # the boost also needs the pinned kernel (newer kernels don't need it). @@ -131,9 +194,9 @@ show_menu() { local tree=""; [[ -n "${PARENT[$c]:-}" ]] && tree=" └ " printf " %b%-3s %-6s %-6s %s%s (not available)%b\n" "$c_dim" "$i" "$now" "$want" "$tree" "${LABEL[$c]}" "$c_reset" elif [[ -n "${PARENT[$c]:-}" ]]; then - printf " %-3s %b %-6s └ %s\n" "$i" "$now" "$want" "${LABEL[$c]}" + printf " %-3s %b %-6s └ %b\n" "$i" "$now" "$want" "$(menu_label "$c")" else - printf " %-3s %b %-6s %s\n" "$i" "$now" "$want" "${LABEL[$c]}" + printf " %-3s %b %-6s %b\n" "$i" "$now" "$want" "$(menu_label "$c")" fi done echo @@ -266,7 +329,7 @@ plan_changes() { component_available "$c" || continue [[ "${WANTED[$c]}" == 1 ]] || continue if is_action "$c"; then TO_ENABLE+=("$c"); continue; fi - if [[ "${CURRENT[$c]}" == 0 || "$REAPPLY" == true ]] || + if [[ "${CURRENT[$c]}" == 0 || "$REAPPLY" == true ]] || feature_outdated "$c" || [[ "$c" == gaming && "${CURRENT[single]}" != "${WANTED[single]}" ]]; then TO_ENABLE+=("$c") fi @@ -281,13 +344,13 @@ apply_changes() { for c in "${TO_DISABLE[@]}"; do if [[ "$c" == boot ]]; then echo; echo -e "${c_bold}Boot into: gamescope${c_reset}" else echo; echo -e "${c_bold}Turning off: ${LABEL[$c]}${c_reset}"; fi - "${c}_disable" || failed+=("$c") + if "${c}_disable"; then feature_record "$c" disable; else failed+=("$c"); fi done for c in "${TO_ENABLE[@]}"; do if [[ "$c" == boot ]]; then echo; echo -e "${c_bold}Boot into: desktop${c_reset}" elif is_action "$c"; then echo; echo -e "${c_bold}Running: ${LABEL[$c]%%:*}${c_reset}" else echo; echo -e "${c_bold}Turning on: ${LABEL[$c]}${c_reset}"; fi - "${c}_enable" || failed+=("$c") + if "${c}_enable"; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi done FAILED=("${failed[@]}") } diff --git a/lib/steam-machine.sh b/lib/steam-machine.sh index 3a5ed5d..f47ab42 100644 --- a/lib/steam-machine.sh +++ b/lib/steam-machine.sh @@ -1,6 +1,7 @@ #!/bin/bash # "Steam Machine support" menu item, only on Valve Fremont hardware: the -# front LED bar driver, LED access for Steam, and steamos-manager. +# front LED bar driver, LED access for Steam and steamos-manager. Its +# sub-option "Power-off fix" is lib/fremont-poweroff.sh. # Sourced by steamify.sh; not meant to be run on its own. detect_valve_fremont() { @@ -14,8 +15,8 @@ detect_valve_fremont() { [[ "$vendor" == "OEM" && "$product" == "F7F" ]] } -# Kernel pinned on a Steam Machine: with newer linux-cachyos releases it -# reboots instead of shutting down. The packages are kept in +# Kernel pinned on a Steam Machine (optional): newer linux-cachyos releases +# rebooted it instead of shutting down, which the power-off fix now handles. The packages are kept in # PINNED_KERNEL_DIR, so re-applying (or reinstalling after an update slipped # through) needs no download. PINNED_KERNEL_VER="7.1.6-1" @@ -148,7 +149,9 @@ remove_kernel_pin() { RESTART_FOR_LOGIN=true } -# "Pin the kernel" menu item, a sub-option of Steam Machine support. +# "Pin the kernel" menu item, a sub-option of Steam Machine support. The +# power-off fix made it unnecessary: only shown while on, to remove it. +kpin_available() { detect_valve_fremont && kpin_status; } kpin_status() { pinned_kernel_installed && grep -Eq '^IgnorePkg\s*=.*\slinux-cachyos(\s|$)' /etc/pacman.conf } @@ -334,13 +337,13 @@ reload_powerdevil() { kernel_overview() { # Per installed kernel: headers (needed to build DKMS modules), the # in-kernel Steam controller driver, and, on a Steam Machine, whether the - # LED driver is built for it. Printed in the menu so it's easy to verify + # LED driver and the power-off fix are built for it. Printed in the menu so it's easy to verify # a kernel update or a newly added kernel got everything. local mark="${c_green}yes${c_reset}" miss="${c_red}no ${c_reset}" leds=false detect_valve_fremont && command -v dkms >/dev/null 2>&1 && leds=true - local kdir k pkg headers hid led running line + local kdir k pkg headers hid led off running line local legend="> = running; controller = Steam controller driver" - [[ "$leds" == true ]] && legend+=", LEDs = LED bar driver built" + [[ "$leds" == true ]] && legend+=", LEDs = LED bar driver built, power-off = power-off fix built" echo -e " ${c_bold}Kernels${c_reset} ($legend)" for kdir in /usr/lib/modules/*/; do k="$(basename "$kdir")" @@ -352,7 +355,8 @@ kernel_overview() { line="$(printf '%-23s %-18s headers %b controller %b' "$k" "$pkg" "$headers" "$hid")" if [[ "$leds" == true ]]; then led="$miss"; dkms status -k "$k" leds-valve-dkms 2>/dev/null | grep -q installed && led="$mark" - line+="$(printf ' LEDs %b' "$led")" + off="$miss"; dkms status -k "$k" "$POWEROFF_DKMS_NAME" 2>/dev/null | grep -q installed && off="$mark" + line+="$(printf ' LEDs %b power-off %b' "$led" "$off")" fi echo -e " ${running}${line}" done diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000..23ee437 --- /dev/null +++ b/patches/README.md @@ -0,0 +1,10 @@ +# Patches and extra sources + +Files Steamify builds or applies, kept out of the shell code so they can be +read and reviewed on their own. `lib/*.sh` reads them with `patch_file +`; the single-file build (`.github/tools/bundle.sh`) embeds them. + +| File | Used by | What | +|---|---|---| +| `steamify-fremont-poweroff.c` | `lib/fremont-poweroff.sh` (Steam Machine support) | Kernel module (DKMS): clears GPIO 18's S4/S5 wake bit before power-off, so the Steam Machine stays off with recent kernels | +| `cros-ec-cec-single-port.patch` | `lib/cec.sh` (HDMI-CEC) | Makes Valve's Steam Machine CEC driver find amdgpu's HDMI port | diff --git a/patches/cros-ec-cec-single-port.patch b/patches/cros-ec-cec-single-port.patch new file mode 100644 index 0000000..3c21b3f --- /dev/null +++ b/patches/cros-ec-cec-single-port.patch @@ -0,0 +1,21 @@ +Steam Machine CEC: find amdgpu's HDMI port with a single CEC port. + +amdgpu registers its HDMI notifier without a port name, which only matches +the driver's named lookup ("Port C") if the CEC driver loaded first; amdgpu +loads from the initramfs, so it never does and /dev/cec0 stays at f.f.f.f. +With a single CEC port, look the notifier up by device alone. + +Applies to Valve's drivers/media/cec/platform/cros-ec/cros-ec-cec.c +(evlaV/linux-integration 10c8c880, see CEC_DRIVER_URL in lib/cec.sh). + +--- a/cros-ec-cec.c ++++ b/cros-ec-cec.c +@@ -485,7 +485,7 @@ + goto out_probe_adapter; + } + +- port->notify = cec_notifier_cec_adap_register(hdmi_dev, conns[port_num], ++ port->notify = cec_notifier_cec_adap_register(hdmi_dev, conns[1] ? conns[port_num] : NULL, + port->adap); + if (!port->notify) { + ret = -ENOMEM; diff --git a/patches/steamify-fremont-poweroff.c b/patches/steamify-fremont-poweroff.c new file mode 100644 index 0000000..b7c7c8a --- /dev/null +++ b/patches/steamify-fremont-poweroff.c @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Steam Machine (Fremont): power off instead of booting up again. + * + * Since "pinctrl-amd: Don't clear S4 wake bits at probe" (Linux 7.2, also + * backported to 6.x, 7.0 and 7.1 updates) the S4/S5 wake bit the firmware + * leaves set on GPIO pin 18 (_SB.PCI0.GPP6) stays set, and the machine starts again right after powering off. + * Valve's kernel clears that bit at probe on Fremont (not for upstream); + * this does the same right before power-off, for kernels without it. + */ +#include +#include +#include +#include +#include +#include +#include + +#define FREMONT_GPP6_PIN 18 +#define WAKE_CNTRL_OFF_S4 15 + +static void __iomem *pin_reg; +static struct sys_off_handler *handler; + +static int fremont_poweroff_prepare(struct sys_off_data *data) +{ + u32 v = readl(pin_reg); + + if (v & BIT(WAKE_CNTRL_OFF_S4)) + writel(v & ~BIT(WAKE_CNTRL_OFF_S4), pin_reg); + return NOTIFY_DONE; +} + +static int __init fremont_poweroff_init(void) +{ + struct acpi_device *adev; + struct device *dev; + struct resource *res; + u32 v; + + if (!dmi_match(DMI_BOARD_NAME, "Fremont")) + return -ENODEV; + adev = acpi_dev_get_first_match_dev("AMDI0030", NULL, -1); + if (!adev) + return -ENODEV; + dev = bus_find_device_by_acpi_dev(&platform_bus_type, adev); + acpi_dev_put(adev); + if (!dev) + return -ENODEV; + res = platform_get_resource(to_platform_device(dev), IORESOURCE_MEM, 0); + put_device(dev); + if (!res || resource_size(res) < (FREMONT_GPP6_PIN + 1) * 4) + return -ENODEV; + /* pinctrl-amd owns the region; only this one register is touched. */ + pin_reg = ioremap(res->start + FREMONT_GPP6_PIN * 4, 4); + if (!pin_reg) + return -ENOMEM; + v = readl(pin_reg); + pr_info("GPIO %d register 0x%08x, S4/S5 wake %s\n", FREMONT_GPP6_PIN, v, + v & BIT(WAKE_CNTRL_OFF_S4) ? "set (cleared at power-off)" : "clear"); + handler = register_sys_off_handler(SYS_OFF_MODE_POWER_OFF_PREPARE, + SYS_OFF_PRIO_DEFAULT, fremont_poweroff_prepare, NULL); + if (IS_ERR(handler)) { + iounmap(pin_reg); + return PTR_ERR(handler); + } + return 0; +} + +static void __exit fremont_poweroff_exit(void) +{ + unregister_sys_off_handler(handler); + iounmap(pin_reg); +} + +module_init(fremont_poweroff_init); +module_exit(fremont_poweroff_exit); +MODULE_DESCRIPTION("Steam Machine: clear GPIO 18's S4 wake bit before power-off"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("dmi:*:rnFremont:*"); diff --git a/steamify.sh b/steamify.sh index 4eb66f4..ea40854 100755 --- a/steamify.sh +++ b/steamify.sh @@ -15,11 +15,11 @@ set -uo pipefail # Release version, see CHANGELOG.md. -VERSION=2.1.0 +VERSION=2.2.0 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -for lib in common state packages login-manager single-user steam-desktop steam-machine hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu backend; do +for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu backend; do # shellcheck source=/dev/null source "$SCRIPT_DIR/lib/$lib.sh" done @@ -139,6 +139,7 @@ while true; do for c in "${TO_ENABLE[@]}"; do if [[ "$c" == boot ]]; then echo " - boot into: desktop (from the next boot)" elif is_action "$c"; then echo " - run: ${LABEL[$c]%%:*} at your own risk (checks, then asks twice more)" + elif feature_outdated "$c"; then echo " - update: ${LABEL[$c]} (changed in this version)" elif [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}"; else echo " - turn on: ${LABEL[$c]}"; fi done ask_yn "Go ahead?" y || { info "Nothing changed."; continue; } diff --git a/ui/qml/Main.qml b/ui/qml/Main.qml index fc8ce49..9f2f700 100644 --- a/ui/qml/Main.qml +++ b/ui/qml/Main.qml @@ -69,8 +69,11 @@ ApplicationWindow { machine: { label: "Steam Machine support", hint: "LED bar, fan and performance settings in Steam", body: "The front LED bar works, Steam's hardware settings work, and the power button puts it to sleep like a console.", changes: ["leds-valve driver for every kernel (DKMS)", "steamos-manager for Steam's settings", "Console-like power handling"] }, - kpin: { label: "Pin the kernel", hint: "Fixes rebooting after shutdown", - body: "Newer CachyOS kernels make the Steam Machine reboot instead of shutting down. Untick once CachyOS fixes that.", + poweroff: { label: "Power-off fix", hint: "Stays off after shutting down", + body: "With recent kernels the Steam Machine starts again right after shutting down: the firmware leaves a wake bit set, and newer kernels (7.2, and updates of 6.x, 7.0 and 7.1) no longer clear it. Valve's own kernel clears it; this small module does the same right before power-off.", + changes: ["steamify-fremont-poweroff module for each installed kernel (DKMS)", "Only on a Steam Machine, only touches that one wake bit", "Off: recent kernels may start it again after shutting down"] }, + kpin: { label: "Pin the kernel", hint: "Untick for CachyOS's current kernel", + body: "Keeps the Steam Machine on CachyOS kernel 7.1.6. Untick it to go back to CachyOS's current kernel; HDMI refresh boost goes with it.", changes: ["linux-cachyos from Steamify's release (signature checked)", "Kept in /var/cache/steamify/kernel", "Added to IgnorePkg"] }, hdmi: { label: "HDMI refresh boost", hint: "Higher refresh rates over HDMI", body: "The pinned kernel keeps many HDMI displays at 60 Hz. Turning this on shows which refresh rates your display can run at the desktop resolution; you pick them, and each one is tried for 15 seconds so you can check the picture before it's installed.", @@ -247,7 +250,9 @@ ApplicationWindow { if (id === "hdmi" && !hdmiChoice && hdmiSaved.length) { openHdmiList(); return; } if (id === "hdmi" && w.hdmi && !nowOn("hdmi") && !hdmiChoice) { startHdmi(); return; } if (id === "hdmi" && !w.hdmi) hdmiChoice = ""; - if (id === "machine") w.kpin = w.machine; + if (id === "machine") w.poweroff = w.machine; + if (id === "poweroff" && w.poweroff) w.machine = true; + if (id === "machine" && !w.machine) w.kpin = false; if (id === "kpin" && w.kpin) w.machine = true; if (id === "hdmi" && w.hdmi) { w.machine = true; w.kpin = true; } if (!w.machine || !w.kpin) w.hdmi = false; @@ -261,7 +266,7 @@ ApplicationWindow { if (it.parent && !want[it.parent]) { if (it.on) p.push({ id: it.id, action: "off" }); continue; } if (want[it.id] && !it.on) p.push({ id: it.id, action: "on" }); else if (!want[it.id] && it.on) p.push({ id: it.id, action: "off" }); - else if (want[it.id] && reapply) p.push({ id: it.id, action: "again" }); + else if (want[it.id] && (reapply || it.update)) p.push({ id: it.id, action: reapply ? "again" : "update" }); } var bootNow = nowOn("boot") ? "desktop" : "gamescope"; if (want.gaming && boot !== bootNow) p.push({ id: "boot", action: boot === "desktop" ? "desktop" : "gaming" }); @@ -622,7 +627,11 @@ ApplicationWindow { x: row.modelData.parent ? 46 : 16 width: controls.x - x - 16 opacity: row.modelData.kind === "action" && !(bios && bios.selectable) ? 0.6 : 1 - Text { text: label(row.modelData); color: t.textHi; font.family: t.body; font.pixelSize: 17; font.weight: Font.DemiBold; elide: Text.ElideRight; width: parent.width } + // The name, with an Update badge when a newer version of it will be applied. + Row { width: parent.width; spacing: 10 + Text { text: label(row.modelData); color: t.textHi; font.family: t.body; font.pixelSize: 17; font.weight: Font.DemiBold; elide: Text.ElideRight + width: Math.min(implicitWidth, parent.width - (upd.visible ? upd.width + parent.spacing : 0)) } + Chip { id: upd; visible: !!row.modelData.update; text: "Update"; fg: t.warn; bgc: t.warnBg; height: 20; anchors.verticalCenter: parent.verticalCenter } } Text { text: row.modelData.id === "bios" ? biosHint() : ((row.modelData.id === "hdmi" && hdmiRowHint()) || (texts[row.modelData.id] && texts[row.modelData.id].hint) || row.modelData.hint); color: t.mute; font.family: t.body; font.pixelSize: 13; elide: Text.ElideRight; width: parent.width } } // Right: every control ends on the same edge @@ -748,7 +757,7 @@ ApplicationWindow { model: plan ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } delegate: Rectangle { required property var modelData; width: 740; height: 56; radius: 12; color: t.card - readonly property var st: ({ on: ["Turn on", t.good, t.goodBg], off: ["Turn off", t.bad, t.badBg], again: ["Re-apply", "#7cc4ff", "#1b2b40"], + readonly property var st: ({ on: ["Turn on", t.good, t.goodBg], off: ["Turn off", t.bad, t.badBg], again: ["Re-apply", "#7cc4ff", "#1b2b40"], update: ["Update", t.warn, t.warnBg], desktop: ["Desktop", "#7cc4ff", "#1b2b40"], gaming: ["Gaming", "#7cc4ff", "#1b2b40"], check: ["Check", t.warn, t.warnBg], flash: ["Flash", t.bad, t.badBg] })[modelData.action] || ["", t.text, t.card] Row { anchors.fill: parent; anchors.leftMargin: 18; spacing: 14 @@ -761,10 +770,10 @@ ApplicationWindow { Text { anchors.verticalCenter: parent.verticalCenter; x: 18; text: "Your password is asked once. Changes to how the PC starts need a restart."; color: t.soft; font.family: t.body; font.pixelSize: 14 } } } Rectangle { - x: parent.width - 468; y: 32; width: 428; height: 210; radius: 16; color: t.card + x: parent.width - 468; y: 32; width: 428; height: 250; radius: 16; color: t.card Column { anchors.fill: parent; anchors.margins: 24; spacing: 12 Text { text: "SUMMARY"; color: t.faint; font.family: t.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 } - Repeater { model: [["Turn on", "on"], ["Re-apply", "again"], ["Turn off", "off"]] + Repeater { model: [["Turn on", "on"], ["Update", "update"], ["Re-apply", "again"], ["Turn off", "off"]] Row { required property var modelData; width: 380 Text { text: parent.modelData[0]; color: t.soft; font.family: t.body; font.pixelSize: 15; width: 300 } Text { text: plan.filter(function (p) { return p.action === parent.modelData[1]; }).length; color: t.text; font.family: t.mono; font.pixelSize: 15; width: 80; horizontalAlignment: Text.AlignRight } } } @@ -849,8 +858,15 @@ ApplicationWindow { border.width: parent.parent.st === "wait" || parent.parent.st === "run" ? 2 : 0; border.color: parent.parent.st === "run" ? t.accent : "#343f50" Text { anchors.centerIn: parent; text: parent.parent.parent.st === "ok" ? "✓" : (parent.parent.parent.st === "fail" ? "!" : ""); color: parent.parent.parent.st === "ok" ? t.good : t.bad; font.pixelSize: 13; font.weight: Font.Bold } RotationAnimator on rotation { running: parent.parent.parent.st === "run"; from: 0; to: 360; duration: 1000; loops: Animation.Infinite } } - Text { text: ({ on: "Turn on ", off: "Turn off ", again: "Re-apply ", desktop: "Boot into ", gaming: "Boot into ", check: "Download and check the ", flash: "Hand to fwupd: the " })[parent.parent.modelData.action] + ((texts[parent.parent.modelData.id] || {}).label || parent.parent.modelData.id) - color: parent.parent.st === "wait" ? t.faint : t.textHi; font.family: t.body; font.pixelSize: 16; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter; width: 380; elide: Text.ElideRight } + // The step; an update shows the name with an Update badge, like the menu. + Item { width: 380; height: parent.height + readonly property var step: parent.parent.modelData + readonly property bool update: step.action === "update" + Row { anchors.verticalCenter: parent.verticalCenter; width: parent.width; spacing: 10 + Text { text: (({ on: "Turn on ", off: "Turn off ", again: "Re-apply ", desktop: "Boot into ", gaming: "Boot into ", check: "Download and check the ", flash: "Hand to fwupd: the " })[parent.parent.step.action] || "") + ((texts[parent.parent.step.id] || {}).label || parent.parent.step.id) + color: parent.parent.parent.parent.st === "wait" ? t.faint : t.textHi; font.family: t.body; font.pixelSize: 16; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter; elide: Text.ElideRight + width: Math.min(implicitWidth, parent.width - (stepUpd.visible ? stepUpd.width + parent.spacing : 0)) } + Chip { id: stepUpd; visible: parent.parent.update; text: "Update"; fg: t.warn; bgc: t.warnBg; height: 20; anchors.verticalCenter: parent.verticalCenter } } } Text { text: ({ wait: "Waiting", run: "Working…", ok: "Done", fail: "Problem" })[parent.parent.st]; color: parent.parent.st === "ok" ? t.good : (parent.parent.st === "fail" ? t.bad : "#b8c3d1"); font.family: t.body; font.pixelSize: 13; anchors.verticalCenter: parent.verticalCenter } } } diff --git a/ui/steamify-app.sh b/ui/steamify-app.sh index 6cafcec..35a267b 100755 --- a/ui/steamify-app.sh +++ b/ui/steamify-app.sh @@ -2,7 +2,7 @@ # Steamify CachyOS - get and start the app (v2), for `curl | bash`: # curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify-app.sh | bash # Downloads the newest release's app package (steamify-app.tar.gz: the app, -# steamify.sh and lib/) to ~/.local/share/steamify/app, installs PySide6 if +# steamify.sh, lib/ and patches/) to ~/.local/share/steamify/app, installs PySide6 if # it's missing, adds a launcher entry, and starts the app. # STEAMIFY_RELEASE= takes that release instead of the newest (the # v2-ui-preview build's copy of this script sets it). STEAMIFY_BRANCH= diff --git a/ui/steamify-ui b/ui/steamify-ui index f0913f4..0bc994e 100755 --- a/ui/steamify-ui +++ b/ui/steamify-ui @@ -197,19 +197,19 @@ class Backend(QObject): p.start() def _make_askpass(self, password): - """A private folder with the password, a helper that prints it for - `sudo -A`, and a `sudo` that always uses the helper (makepkg and - yay call plain sudo).""" + """A private folder with a helper that `sudo -A` runs for the + password, and a `sudo` that always uses it (makepkg and yay call + plain sudo). The password is never written to a file: the helper + reads it from a named pipe, and the app writes it into the pipe from + memory each time sudo asks, until _drop_askpass.""" base = os.environ.get("XDG_RUNTIME_DIR") or tempfile.gettempdir() d = tempfile.mkdtemp(prefix="steamify-", dir=base) os.chmod(d, stat.S_IRWXU) - secret = os.path.join(d, "secret") - fd = os.open(secret, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) - with os.fdopen(fd, "w") as f: - f.write(password + "\n") + pipe = os.path.join(d, "pipe") + os.mkfifo(pipe, 0o600) askpass = os.path.join(d, "askpass") with open(askpass, "w") as f: - f.write("#!/bin/sh\nexec cat %s\n" % json.dumps(secret)) + f.write("#!/bin/sh\nexec cat %s\n" % json.dumps(pipe)) os.chmod(askpass, 0o700) bindir = os.path.join(d, "bin") os.mkdir(bindir) @@ -217,9 +217,35 @@ class Backend(QObject): with open(wrapper, "w") as f: f.write("#!/bin/sh\nexec /usr/bin/sudo -A \"$@\"\n") os.chmod(wrapper, 0o700) + stop = threading.Event() + data = (password + "\n").encode() + + def serve(): + while not stop.is_set(): + try: + fd = os.open(pipe, os.O_WRONLY) # waits for a reader + except OSError: + return + try: + if not stop.is_set(): + os.write(fd, data) + except OSError: + pass + finally: + os.close(fd) + threading.Thread(target=serve, daemon=True).start() + self._askpass_stop = (stop, pipe) return d, askpass, bindir def _drop_askpass(self): + if getattr(self, "_askpass_stop", None): + stop, pipe = self._askpass_stop + self._askpass_stop = None + stop.set() + try: # wakes the writer waiting for a reader, which then ends + os.close(os.open(pipe, os.O_RDONLY | os.O_NONBLOCK)) + except OSError: + pass if self._secret_dir: shutil.rmtree(self._secret_dir, ignore_errors=True) self._secret_dir = None