Files
steamify-cachyos/lib/backend.sh
T

199 lines
8.5 KiB
Bash

#!/bin/bash
# Machine-readable mode for the graphical app (steamify-ui): the same
# components and plan as the menu, as JSON on stdout.
# steamify.sh --backend status
# one JSON object: version, machine, and every menu item with its
# current state and what a run would pick by default.
# steamify.sh --backend apply [--reapply] [--boot gamescope|desktop] <id>...
# turns on the listed components and off the others (like ticking them
# in the menu), then streams one JSON object per line: plan, start,
# log, done, finished. sudo asks through $SUDO_ASKPASS (no terminal).
# steamify.sh --backend bios-prepare | bios-flash
# the BIOS update in two steps, so the app shows both warnings between
# them (bios-ready event: current, newest, checksum, compatible).
# Sourced by steamify.sh; not meant to be run on its own.
json_str() {
# json_str <text>: <text> as a JSON string (quotes, backslashes and
# control characters escaped; colour codes dropped).
local s="$1"
s="$(printf '%s' "$s" | sed -E $'s/\x1b\\[[0-9;]*[A-Za-z]//g')"
s="${s//\\/\\\\}"; s="${s//\"/\\\"}"
s="${s//$'\t'/\\t}"; s="${s//$'\r'/}"; s="${s//$'\n'/\\n}"
s="$(printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037')"
printf '"%s"' "$s"
}
json_list() {
# json_list <item>...: a JSON array of strings.
local out="" x
for x in "$@"; do out+="${out:+,}$(json_str "$x")"; done
printf '[%s]' "$out"
}
json_bool() {
# json_bool <command>...: true or false by the command's exit status.
if "$@"; then printf true; else printf false; fi
}
json_item_head() {
# json_item_head <id> <kind>: the start of an item's object (no closing
# brace) with what the app and the installer page both read: id, label,
# hint (the part of the label after the colon), kind, parent.
local c="$1" hint=""
[[ "${LABEL[$c]}" == *:* ]] && hint="${LABEL[$c]#*: }"
printf '{"id":%s,"label":%s,"hint":%s,"kind":"%s","parent":%s,"needs":""' \
"$(json_str "$c")" "$(json_str "${LABEL[$c]%%:*}")" "$(json_str "$hint")" "$2" "$(json_str "${PARENT[$c]:-}")"
}
backend_event() {
# backend_event <event> [<json fields without braces>]
printf '{"event":%s%s}\n' "$(json_str "$1")" "${2:+,$2}"
}
backend_status() {
local c first_run=true items="" kind
detect_components
for c in "${COMPONENTS[@]}"; do
component_available "$c" && ! is_action "$c" && is_current "$c" && first_run=false
done
for c in "${COMPONENTS[@]}"; do
component_available "$c" || continue
kind=toggle; is_action "$c" && kind=action; [[ "$c" == boot ]] && kind=choice
items+="${items:+,}$(json_item_head "$c" "$kind")"
items+=",\"on\":$(json_bool is_current "$c")"
items+=",\"wanted\":$(json_bool is_wanted "$c")"
items+=",\"update\":$(json_bool feature_outdated "$c")"
items+=",\"new\":$(json_bool feature_any_new "$c")"
items+=",\"selectable\":$(json_bool component_selectable "$c")"
# Why it can't be turned on, for the app's explanation.
[[ "$c" == vram && -n "${VRAM_NVIDIA_CASE:-}" ]] &&
items+=",\"note\":$(json_str "$(vram_nvidia_note "$VRAM_NVIDIA_CASE")")"
items+="}"
done
local cec="" f
for f in /dev/cec*; do [[ -e "$f" ]] && cec+="${cec:+ }$(basename "$f")"; done
local bios='null'
if bios_available; then
bios="{\"current\":$(json_str "$(bios_current)"),\"newest\":$(json_str "${BIOS_NEWEST:-}"),\"selectable\":$(json_bool bios_selectable),\"dryRun\":$([[ -n "$BIOS_DRY_RUN" ]] && echo true || echo false)}"
fi
printf '{"version":%s,"firstRun":%s,"steamMachine":%s,"kernel":%s,"pinnedKernel":%s,"cecDevices":%s,"leds":%s,"bios":%s,"items":[%s]}\n' \
"$(json_str "$VERSION")" "$first_run" \
"$(json_bool detect_valve_fremont)" \
"$(json_str "$(uname -r)")" "$(json_str "${PINNED_KERNEL_VER:-}")" \
"$(json_str "$cec")" \
"$(compgen -G '/sys/class/leds/valve-leds*' | wc -l)" \
"$bios" "$items"
}
backend_run_component() {
# backend_run_component <id> <enable|disable>: runs it in this shell
# (its variables, e.g. RESTART_FOR_LOGIN, must survive), each output line
# as a log event.
local c="$1" what="$2" rc
backend_event start "\"id\":$(json_str "$c"),\"action\":\"$what\",\"label\":$(json_str "${LABEL[$c]%%:*}")"
"${c}_${what}" > >(while IFS= read -r line; do backend_event log "\"id\":$(json_str "$c"),\"line\":$(json_str "$line")"; done) 2>&1
rc=$?
# Let the log reader finish before the next event.
wait $! 2>/dev/null
backend_event "done" "\"id\":$(json_str "$c"),\"ok\":$([[ $rc -eq 0 ]] && echo true || echo false)"
return $rc
}
backend_sudo() {
# sudo from the app has no terminal: ask through its helper, and keep the
# credentials fresh as the menu does.
if [[ -z "${SUDO_ASKPASS:-}" ]] && ! sudo -n true 2>/dev/null; then
backend_event finished '"failed":[],"restart":false,"error":"no-sudo"'
return 1
fi
sudo() { command sudo ${SUDO_ASKPASS:+-A} "$@"; }
if ! sudo -n true 2>/dev/null && ! sudo true 2>/dev/null; then
backend_event finished '"failed":[],"restart":false,"error":"wrong-password"'
return 1
fi
while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null &
}
backend_bios_prepare() {
# Download, checksum and fwupd's device check; the app then shows its
# two warnings and calls bios-flash.
local rc
backend_sudo || return 1
backend_event start '"id":"bios","action":"prepare","label":"Update BIOS"'
bios_prepare > >(while IFS= read -r line; do backend_event log "\"id\":\"bios\",\"line\":$(json_str "$line")"; done) 2>&1
rc=$?; wait $! 2>/dev/null
case $rc in
0) backend_event bios-ready "\"current\":$(json_str "$(bios_current)"),\"newest\":$(json_str "$BIOS_NEWEST"),\"checksum\":true,\"compatible\":$(json_str "$BIOS_COMPATIBLE")" ;;
2) backend_event finished '"failed":[],"restart":false,"nothing":true' ;;
*) backend_event finished '"failed":["bios"],"restart":false' ;;
esac
}
backend_bios_flash() {
backend_sudo || return 1
local rc
backend_run_component bios flash_only; rc=$?
backend_event finished "\"failed\":$([[ $rc -eq 0 ]] && echo '[]' || echo '["bios"]'),\"restart\":$(restart_needed && echo true || echo false),\"bios\":true"
}
backend_apply() {
local reapply=false boot="" id c
local -a want=()
while [[ $# -gt 0 ]]; do
case "$1" in
--reapply) reapply=true ;;
--boot) boot="$2"; shift ;;
*) want+=("$1") ;;
esac
shift
done
detect_components
# What the app ticked: exactly these on, every other component off.
for c in "${COMPONENTS[@]}"; do
component_available "$c" || continue
is_action "$c" && { WANTED[$c]=0; continue; }
WANTED[$c]=0
for id in "${want[@]}"; do [[ "$id" == "$c" ]] && WANTED[$c]=1; done
done
normalize_wanted
# Retired: only ever removed.
WANTED[hdmi]=0
case "$boot" in desktop) WANTED[boot]=1; WANTED[gaming]=1 ;; gamescope) WANTED[boot]=0 ;; esac
REAPPLY=$reapply
plan_changes
backend_event plan "\"disable\":$(json_list "${TO_DISABLE[@]}"),\"enable\":$(json_list "${TO_ENABLE[@]}")"
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
feature_record_unticked
backend_event finished '"failed":[],"restart":false,"nothing":true'
return 0
fi
backend_sudo || return 1
LOGIN_MANAGER=plasmalogin
is_wanted single && LOGIN_MANAGER=sddm
local -a failed=()
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
feature_record_unticked
os_version_refresh
[[ " ${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)"
}
backend_main() {
case "${1:-}" in
status) backend_status ;;
apply) shift; backend_apply "$@" ;;
bios-prepare) backend_bios_prepare ;;
bios-flash) backend_bios_flash ;;
*) err "Usage: steamify.sh --backend status | apply [--reapply] [--boot gamescope|desktop] <id>..."; return 2 ;;
esac
}