cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

release/portal-packaging
goq / scripts / bazzite-inventory.sh
bazzite-inventory.sh 16.3K executable
1#!/usr/bin/env bash2set -euo pipefail34section() {5 printf '\n## %s\n' "$1"6}78optional() {9 local command_name="$1"10 shift11 if command -v "$command_name" >/dev/null 2>&1; then12 "$@" || printf 'command_failed=%s status=%s\n' "$command_name" "$?"13 else14 printf 'command_missing=%s\n' "$command_name"15 fi16}1718probe_h264_vaapi_render_node() {19 local ffmpeg_path="$1"20 local candidate="$2"2122 "$ffmpeg_path" -hide_banner -loglevel error \23 -vaapi_device "$candidate" \24 -f lavfi -i color=size=128x128:rate=1 \25 -vf 'format=nv12,hwupload' \26 -c:v h264_vaapi -frames:v 1 -f null -27}2829select_h264_vaapi_render_node() {30 local device_root="${1:-/dev/dri}"31 local require_character="${2:-true}"32 local ffmpeg_path33 local candidate3435 render_node=''36 ffmpeg_path="$(command -v ffmpeg)" || return 137 for candidate in "$device_root"/renderD*; do38 [[ "$(basename "$candidate")" =~ ^renderD[0-9]+$ ]] || continue39 [[ -e "$candidate" && ! -L "$candidate" && -r "$candidate" && -w "$candidate" ]] \40 || continue41 if [[ "$require_character" == true && ! -c "$candidate" ]]; then42 continue43 fi44 if probe_h264_vaapi_render_node "$ffmpeg_path" "$candidate" \45 >/dev/null 2>&146 then47 render_node="$candidate"48 return 049 fi50 done51 return 152}5354gstreamer_inventory() {55 if ! command -v gst-inspect-1.0 >/dev/null 2>&1; then56 echo 'gstreamer_inventory=unavailable'57 return58 fi5960 local element61 for element in appsink pipewiresrc queue videoconvert videoscale videorate h264parse fdsink; do62 if gst-inspect-1.0 "$element" >/dev/null 2>&1; then63 printf 'gstreamer_element=%s status=present\n' "$element"64 else65 printf 'gstreamer_element=%s status=missing\n' "$element"66 fi67 done68 gst-inspect-1.0 pipewiresrc 2>/dev/null || true69 while IFS= read -r encoder; do70 printf 'gstreamer_va_h264_factory=%s\n' "$encoder"71 gst-inspect-1.0 "$encoder" 2>/dev/null || true72 done < <(73 gst-inspect-1.0 2>/dev/null | awk '74 $2 ~ /^va(renderD[0-9]+)?h264(lp)?enc:$/ {75 sub(/:$/, "", $2)76 print $277 }78 '79 )80}8182gstreamer_development_inventory() {83 if ! command -v pkg-config >/dev/null 2>&1; then84 echo 'gstreamer_development_metadata=unavailable'85 return86 fi8788 local module89 local version90 for module in gstreamer-1.0 gstreamer-app-1.0 gstreamer-video-1.0; do91 if version="$(pkg-config --modversion "$module" 2>/dev/null)"; then92 printf 'gstreamer_development_module=%s status=present version=%s\n' \93 "$module" "$version"94 else95 printf 'gstreamer_development_module=%s status=missing\n' "$module"96 fi97 done98}99100cold_boot_failures=0101cold_boot_insufficient=0102103cold_boot_failure() {104 printf 'cold_boot_failure=%s\n' "$1"105 ((cold_boot_failures += 1))106}107108cold_boot_evidence_insufficient() {109 printf 'cold_boot_evidence_insufficient=%s\n' "$1"110 ((cold_boot_insufficient += 1))111}112113summary_value() {114 local key="$1"115 sed -n "s/^${key}=//p" | head -n 1116}117118exact_property_value() {119 local document="$1"120 local property="$2"121 local count122 local value123124 count="$(grep -c "^${property}=" <<<"$document" || true)"125 [[ "$count" == 1 ]] || return 1126 value="$(sed -n "s/^${property}=//p" <<<"$document")"127 [[ -n "$value" ]] || return 1128 printf '%s\n' "$value"129}130131monotonic_before() {132 local earlier="$1"133 local later="$2"134 awk -v earlier="$earlier" -v later="$later" \135 'BEGIN { exit !(earlier < later) }'136}137138summarize_boot_journal() {139 awk '140 function monotonic(line, value) {141 value = line142 sub(/^[[:space:]]*\[/, "", value)143 sub(/\].*$/, "", value)144 return value145 }146 {147 timestamp = monotonic($0)148 if (timestamp !~ /^[0-9]+([.][0-9]+)?$/) {149 next150 }151 entries += 1152 if (first == "") {153 first = timestamp154 }155 line = tolower($0)156 if (gamescope == "" && (line ~ /gamescope-session(-plus)?\[[0-9]+\]/ || line ~ /started .*gamescope/)) {157 gamescope = timestamp158 }159 if (sigil_start == "" && line ~ /started sigil-host[.]service/) {160 sigil_start = timestamp161 }162 if (sigil_ready == "" && line ~ /sigil host ready/) {163 sigil_ready = timestamp164 }165 if (ssh_accept == "" && line ~ /accepted (publickey|password)/) {166 ssh_accept = timestamp167 }168 }169 END {170 printf "journal_entries=%d\n", entries171 printf "journal_first_monotonic_seconds=%s\n", first172 printf "gamescope_first_monotonic_seconds=%s\n", gamescope173 printf "sigil_unit_first_monotonic_seconds=%s\n", sigil_start174 printf "sigil_ready_first_monotonic_seconds=%s\n", sigil_ready175 printf "ssh_accept_first_monotonic_seconds=%s\n", ssh_accept176 }177 '178}179180cold_boot_connector_evidence() {181 local connector_status182 local connector_name183 local status184 local found=false185 local connected=false186187 for connector_status in /sys/class/drm/card*-*/status; do188 [[ -e "$connector_status" ]] || continue189 found=true190 connector_name="$(basename "$(dirname "$connector_status")")"191 status="$(tr -d '\n' < "$connector_status")"192 printf 'drm_connector=%s status=%s\n' "$connector_name" "$status"193 if [[ "$status" == connected ]]; then194 connected=true195 cold_boot_failure "physical_connector_connected:$connector_name"196 fi197 done198199 if [[ "$found" == false ]]; then200 cold_boot_evidence_insufficient no_drm_connector_status_files201 elif [[ "$connected" == false ]]; then202 echo 'headless_connector_state=ok'203 fi204}205206cold_boot_session_evidence() {207 local sessions208 local session_id209 local session_details210 local current_user_id211 local service212 local session_user213 local remote214 local state215 local autologin_found=false216217 current_user_id="$(id -u)"218219 if ! sessions="$(loginctl list-sessions --no-legend 2>&1)"; then220 printf 'loginctl_error=%s\n' "$sessions"221 cold_boot_evidence_insufficient login_sessions_unavailable222 return223 fi224225 while read -r session_id _; do226 [[ -n "$session_id" ]] || continue227 if ! session_details="$(loginctl show-session "$session_id" \228 -p Id -p User -p Remote -p Service -p Type -p Class -p State -p Timestamp 2>&1)"; then229 printf 'session_id=%s details=unavailable\n' "$session_id"230 cold_boot_evidence_insufficient "session_details_unavailable:$session_id"231 continue232 fi233 printf 'session_begin=%s\n%s\nsession_end=%s\n' \234 "$session_id" "$session_details" "$session_id"235 service="$(exact_property_value "$session_details" Service)" || continue236 session_user="$(exact_property_value "$session_details" User)" || continue237 remote="$(exact_property_value "$session_details" Remote)" || continue238 state="$(exact_property_value "$session_details" State)" || continue239 if [[ "$service" =~ ^[[:alnum:]_.@+-]+-autologin$ ]] &&240 [[ "$session_user" == "$current_user_id" ]] &&241 [[ "$remote" == no ]] &&242 [[ "$state" == active ]]; then243 autologin_found=true244 fi245 done <<<"$sessions"246247 if [[ "$autologin_found" == true ]]; then248 echo 'gaming_autologin_session=ok'249 else250 cold_boot_failure no_active_local_gaming_autologin_session251 fi252}253254cold_boot_service_evidence() {255 local enabled256 local active257 local service_details258259 enabled="$(systemctl --user is-enabled sigil-host.service 2>&1 || true)"260 active="$(systemctl --user is-active sigil-host.service 2>&1 || true)"261 printf 'sigil_host_enabled=%s\n' "$enabled"262 printf 'sigil_host_active=%s\n' "$active"263 [[ "$enabled" == enabled ]] || cold_boot_failure sigil_host_not_enabled264 [[ "$active" == active ]] || cold_boot_failure sigil_host_not_active265266 if service_details="$(systemctl --user show sigil-host.service \267 -p ActiveEnterTimestamp -p ActiveEnterTimestampMonotonic \268 -p ExecMainStartTimestamp -p FragmentPath -p NRestarts 2>&1)"; then269 printf '%s\n' "$service_details"270 else271 printf 'sigil_host_show_error=%s\n' "$service_details"272 cold_boot_evidence_insufficient sigil_host_service_details_unavailable273 fi274}275276cold_boot_pipewire_evidence() {277 local pipewire_nodes278 local default_sink279280 if ! command -v pw-dump >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then281 cold_boot_evidence_insufficient pipewire_inventory_tools_missing282 return283 fi284285 if ! pipewire_nodes="$(pw-dump 2>/dev/null | jq -r '286 .[]287 | select(.type == "PipeWire:Interface:Node")288 | if (289 .info.props["node.name"] == "gamescope"290 and .info.props["media.class"] == "Video/Source"291 ) then "gamescope_video"292 elif (293 .info.props["node.name"] == "sigil_spark"294 and .info.props["media.class"] == "Audio/Sink"295 ) then "sigil_audio"296 else empty297 end298 ' | sort -u)"; then299 cold_boot_evidence_insufficient pipewire_nodes_unavailable300 return301 fi302303 if grep -qx gamescope_video <<<"$pipewire_nodes"; then304 echo 'gamescope_pipewire_node=ok'305 else306 cold_boot_failure gamescope_pipewire_node_missing307 fi308 if grep -qx sigil_audio <<<"$pipewire_nodes"; then309 echo 'sigil_audio_pipewire_node=ok'310 else311 cold_boot_failure sigil_audio_pipewire_node_missing312 fi313314 if ! command -v pactl >/dev/null 2>&1; then315 cold_boot_evidence_insufficient pactl_missing316 elif default_sink="$(pactl get-default-sink 2>&1)"; then317 printf 'default_audio_sink=%s\n' "$default_sink"318 [[ "$default_sink" == sigil_spark ]] || cold_boot_failure default_audio_sink_not_sigil_spark319 else320 printf 'pactl_error=%s\n' "$default_sink"321 cold_boot_evidence_insufficient default_audio_sink_unavailable322 fi323}324325cold_boot_journal_evidence() {326 local journal_summary327 local journal_entries328 local journal_first329 local gamescope_first330 local sigil_unit_first331 local sigil_ready_first332 local ssh_accept_first333334 if ! journal_summary="$(journalctl -b -o short-monotonic --no-pager 2>/dev/null | \335 summarize_boot_journal)"; then336 cold_boot_evidence_insufficient boot_journal_unavailable337 return338 fi339 printf '%s\n' "$journal_summary"340341 journal_entries="$(summary_value journal_entries <<<"$journal_summary")"342 journal_first="$(summary_value journal_first_monotonic_seconds <<<"$journal_summary")"343 gamescope_first="$(summary_value gamescope_first_monotonic_seconds <<<"$journal_summary")"344 sigil_unit_first="$(summary_value sigil_unit_first_monotonic_seconds <<<"$journal_summary")"345 sigil_ready_first="$(summary_value sigil_ready_first_monotonic_seconds <<<"$journal_summary")"346 ssh_accept_first="$(summary_value ssh_accept_first_monotonic_seconds <<<"$journal_summary")"347348 if [[ ! "$journal_entries" =~ ^[1-9][0-9]*$ ||349 ! "$journal_first" =~ ^[0-9]+([.][0-9]+)?$ ]]; then350 cold_boot_evidence_insufficient boot_journal_empty_or_unparseable351 return352 fi353 if ! awk -v first="$journal_first" 'BEGIN { exit !(first <= 300) }'; then354 cold_boot_evidence_insufficient boot_journal_does_not_reach_startup355 fi356 if [[ ! "$ssh_accept_first" =~ ^[0-9]+([.][0-9]+)?$ ]]; then357 cold_boot_evidence_insufficient first_ssh_acceptance_not_observable358 return359 fi360361 if [[ ! "$gamescope_first" =~ ^[0-9]+([.][0-9]+)?$ ]]; then362 cold_boot_failure gamescope_start_not_found_in_boot_journal363 elif ! monotonic_before "$gamescope_first" "$ssh_accept_first"; then364 cold_boot_failure gamescope_did_not_start_before_first_ssh365 else366 echo 'gamescope_before_first_ssh=ok'367 fi368369 if [[ ! "$sigil_unit_first" =~ ^[0-9]+([.][0-9]+)?$ ]]; then370 cold_boot_failure sigil_unit_start_not_found_in_boot_journal371 elif ! monotonic_before "$sigil_unit_first" "$ssh_accept_first"; then372 cold_boot_failure sigil_unit_did_not_start_before_first_ssh373 else374 echo 'sigil_unit_before_first_ssh=ok'375 fi376377 if [[ ! "$sigil_ready_first" =~ ^[0-9]+([.][0-9]+)?$ ]]; then378 cold_boot_failure sigil_ready_not_found_in_boot_journal379 elif ! monotonic_before "$sigil_ready_first" "$ssh_accept_first"; then380 cold_boot_failure sigil_was_not_ready_before_first_ssh381 else382 echo 'sigil_ready_before_first_ssh=ok'383 fi384}385386cold_boot_audit() {387 local boot_id388389 section cold_boot_evidence390 if boot_id="$(< /proc/sys/kernel/random/boot_id)" && [[ -n "$boot_id" ]]; then391 printf 'boot_id=%s\n' "$boot_id"392 else393 cold_boot_evidence_insufficient boot_id_unavailable394 fi395396 cold_boot_connector_evidence397 cold_boot_session_evidence398 cold_boot_service_evidence399 cold_boot_pipewire_evidence400 cold_boot_journal_evidence401402 printf 'cold_boot_failure_count=%d\n' "$cold_boot_failures"403 printf 'cold_boot_insufficient_count=%d\n' "$cold_boot_insufficient"404 if ((cold_boot_insufficient > 0)); then405 echo 'cold_boot_result=insufficient'406 return 3407 fi408 if ((cold_boot_failures > 0)); then409 echo 'cold_boot_result=fail'410 return 1411 fi412 echo 'cold_boot_result=pass'413}414415if [[ "${SIGIL_INVENTORY_SOURCE_ONLY:-}" == 1 ]]; then416 if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then417 return 0418 fi419 exit 0420fi421422if [[ "$(uname -s)" != Linux ]]; then423 echo "bazzite-inventory must run on the Linux host" >&2424 exit 1425fi426427mode=inventory428if [[ "${1:-}" == "--smoke" && $# -eq 1 ]]; then429 mode=smoke430elif [[ "${1:-}" == "--cold-boot" && $# -eq 1 ]]; then431 mode=cold-boot432elif [[ $# -ne 0 ]]; then433 echo "usage: $0 [--smoke | --cold-boot]" >&2434 exit 2435fi436437if [[ "$mode" == cold-boot ]]; then438 cold_boot_status=0439 cold_boot_audit || cold_boot_status=$?440 exit "$cold_boot_status"441fi442443section identity444date --iso-8601=ns445hostnamectl446uname -a447id448449section image450sed -n -E '/^(NAME|VERSION|VERSION_ID|VARIANT|VARIANT_ID|IMAGE_ID|IMAGE_VERSION)=/p' /etc/os-release451optional rpm-ostree rpm-ostree status -v452optional bootc bootc status453optional mokutil mokutil --sb-state454optional getenforce getenforce455456section storage457df -h / /var "$HOME"458459section network_and_services460optional ss ss -lntup461systemctl list-unit-files --state=enabled --no-pager462loginctl list-sessions --no-legend463464section versions465optional gamescope gamescope --version466optional pipewire pipewire --version467optional ffmpeg ffmpeg -version468optional gst-inspect-1.0 gst-inspect-1.0 --version469optional podman podman --version470optional rustc rustc --version471optional cargo cargo --version472473section gpu474find /dev/dri -maxdepth 1 -mindepth 1 -printf '%M %u %g %p\n' 2>/dev/null | sort || true475render_node=""476for sysfs_node in /sys/class/drm/renderD*; do477 [[ -e "$sysfs_node/device/driver" ]] || continue478 driver="$(basename "$(readlink -f "$sysfs_node/device/driver")")"479 device="$(basename "$(readlink -f "$sysfs_node/device")")"480 printf 'render_node=/dev/dri/%s driver=%s device=%s\n' \481 "$(basename "$sysfs_node")" "$driver" "$device"482done483select_h264_vaapi_render_node /dev/dri true || true484optional lspci lspci -nnk485optional vulkaninfo vulkaninfo --summary486487if [[ -n "$render_node" ]]; then488 printf 'selected_render_node=%s\n' "$render_node"489 [[ -r "$render_node" && -w "$render_node" ]]490 optional udevadm udevadm info --query=property --name="$render_node"491 optional vainfo vainfo --display drm --device "$render_node"492else493 echo "selected_render_node=none" >&2494fi495496section encoders497if command -v ffmpeg >/dev/null 2>&1; then498 ffmpeg -hide_banner -encoders 2>/dev/null | grep -E '(^|[[:space:]])(libx264|h264_(vaapi|amf|nvenc|videotoolbox))([[:space:]]|$)' || true499fi500501section capture_backends502if command -v ffmpeg >/dev/null 2>&1; then503 ffmpeg -hide_banner -devices 2>/dev/null || true504fi505gstreamer_inventory506507section gstreamer_development508gstreamer_development_inventory509510section gamescope_pipewire511systemctl --user status pipewire.service pipewire.socket wireplumber.service --no-pager || true512pgrep -a gamescope || true513pgrep -a steam || true514if command -v pw-dump >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then515 pw-dump | jq '[516 .[]517 | select(.type == "PipeWire:Interface:Node")518 | select(519 .info.props["node.name"] == "gamescope"520 or .info.props["media.class"] == "Video/Source"521 )522 | {523 global_id: .id,524 object_serial: .info.props["object.serial"],525 node_id: .info.props["node.id"],526 node_name: .info.props["node.name"],527 media_class: .info.props["media.class"],528 description: .info.props["node.description"],529 object_path: .info.props["object.path"],530 client_id: .info.props["client.id"]531 }532 ]'533else534 echo "pipewire_node_inventory=unavailable"535fi536537if [[ "$mode" == smoke ]]; then538 section h264_smoke539 if [[ -z "$render_node" ]]; then540 echo "an accessible VA-API H.264 encode-capable render node is required for --smoke" >&2541 exit 1542 fi543 ffmpeg -hide_banner -loglevel warning \544 -vaapi_device "$render_node" \545 -f lavfi -i testsrc2=size=1280x800:rate=60 \546 -vf 'format=nv12,hwupload' \547 -c:v h264_vaapi \548 -frames:v 600 \549 -f null -550 echo "h264_vaapi_smoke=ok"551fi