The audit's third tier: everything between this installer and a fresh machine it has never met. The one path that could cost a person their display: the interview probes Secure Boot with mokutil, which install-packages had not installed yet, so on a minimal base the MOK question silently never fired -- and install-hardware still installed akmod-nvidia and blacklisted nouveau, arming a reboot into an unloadable driver with its fallback disabled. The probe tools (pciutils, mokutil, fwupd) now bootstrap beside gum, and install-hardware re-checks Secure Boot for itself and refuses the driver rather than the display. Secrets leave the checkout: the personal environment moves to ~/.config/panama/env at mode 600 by migration, and .bashrc sources it with a permission check that quietly re-tightens drift. change-settings no longer overwrites /etc/dnf/dnf.conf -- two performance keys are set additively, the defaultyes=True that made every `dnf remove` treat Enter as yes is gone, and a migration strips it from machines that already received it. Package installation survives the world changing: the initial and desktop lists run with --skip-unavailable and a report_missing pass that names what was skipped (resolved through --whatprovides, so capability names like awk do not cry wolf); the openh264, appstream and core-group extras go through soft; RustDesk resolves its RPM for the machine's own architecture; and the Claude Desktop repository script is fetched to a kept file and run, never piped from the network into root. The hardware predicates stop guessing: a wireless mouse's scope=Device battery no longer turns a tower into a laptop, USB-PD-only machines read their power state from the battery's own status instead of being permanently "on AC", the lid falls back to logind's LidClosed where ACPI is silent, and charge limits reach every pack of a two-battery machine in one authorization -- with the reported percentage summed across packs. And the parsers stop assuming this machine: snapper is read through --machine-readable csv with named columns instead of a localized box-drawing table, and reports whether snapshots are even possible so ext4 and unconfigured-btrfs stop looking identical; fprintd is parsed under LC_ALL=C; the hypridle drop-in resolves the binary it points at; the recorder's render node became an "auto" token resolved at record time; update-grub writes the config its firmware actually boots; the nvm prompt hook and the SSH tmux takeover are guarded; hipblas and rocm-opencl move to an opt-in gpu-compute category; and the two interactive python tools' libraries are declared. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
191 lines
7.4 KiB
Bash
Executable File
191 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# What this machine is, asked one yes-or-no question at a time.
|
|
#
|
|
# panama-hw laptop && echo "portable"
|
|
# panama-hw clamshell && panama-lid close
|
|
#
|
|
# Every subcommand exits 0 for yes and 1 for no, prints nothing, and answers
|
|
# correctly on a machine that has none of the hardware in question. That last
|
|
# part is the whole point: a desktop must be able to ask "am I in clamshell
|
|
# mode" and get a calm no rather than an error, because the scripts and
|
|
# services that ask are shared between machines.
|
|
#
|
|
# `--json` answers everything at once, for the health page and for contracts.
|
|
#
|
|
# Detection reads sysfs directly rather than shelling out to lspci or upower:
|
|
# lspci touches PCI config space and wakes a runtime-suspended GPU, which is a
|
|
# real cost to pay for a question asked at every login.
|
|
#
|
|
# Paths are overridable (PANAMA_HW_SYS, PANAMA_HW_ACPI) so the contract can
|
|
# drive fixture trees. Nothing else should set them.
|
|
|
|
set -uo pipefail
|
|
|
|
SYS="${PANAMA_HW_SYS:-/sys}"
|
|
ACPI="${PANAMA_HW_ACPI:-/proc/acpi}"
|
|
|
|
# SMBIOS chassis types that mean "carried around": Portable, Laptop, Notebook,
|
|
# Hand Held, Sub Notebook, Tablet, Convertible, Detachable. A machine that
|
|
# reports something else, or reports nothing, is treated as stationary --
|
|
# guessing "laptop" on an unknown chassis would put battery chrome on a desktop.
|
|
readonly PORTABLE_CHASSIS=" 8 9 10 11 14 30 31 32 "
|
|
|
|
is_laptop() {
|
|
local type_file="$SYS/class/dmi/id/chassis_type" chassis
|
|
[[ -r "$type_file" ]] || return 1
|
|
chassis="$(cat "$type_file" 2>/dev/null)" || return 1
|
|
[[ "$PORTABLE_CHASSIS" == *" $chassis "* ]]
|
|
}
|
|
|
|
# The first SYSTEM battery, or nothing. Named rather than assumed to be BAT0:
|
|
# the second battery in a ThinkPad is BAT1, and a machine with only BAT1
|
|
# exists. The scope check is what keeps a desktop a desktop: a wireless mouse
|
|
# or a game controller publishes type=Battery with scope=Device, and counting
|
|
# one turned a tower into a "laptop" whose battery readout was the mouse's.
|
|
battery_path() {
|
|
local supply type scope
|
|
for supply in "$SYS"/class/power_supply/*; do
|
|
[[ -r "$supply/type" ]] || continue
|
|
type="$(cat "$supply/type" 2>/dev/null)"
|
|
[[ "$type" == "Battery" ]] || continue
|
|
scope="$(cat "$supply/scope" 2>/dev/null || echo System)"
|
|
[[ "$scope" == "Device" ]] && continue
|
|
printf '%s\n' "$supply"
|
|
return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
has_battery() { battery_path >/dev/null; }
|
|
|
|
# On wall power. A machine with no mains supply at all and no system battery
|
|
# is a desktop, and a desktop is always on wall power -- answering "no" there
|
|
# would make every battery-aware timing apply to a machine that cannot run
|
|
# out of power. But "no Mains" alone is not "desktop": hardware charged only
|
|
# over USB-PD exposes type=USB supplies and no Mains at all, and reading that
|
|
# as permanently-on-AC meant its battery timings never engaged while it ran
|
|
# down. When no Mains exists but a system battery does, the battery's own
|
|
# status is the answer: Discharging means battery, everything else means fed.
|
|
on_ac() {
|
|
local supply type online found=1 battery status
|
|
for supply in "$SYS"/class/power_supply/*; do
|
|
[[ -r "$supply/type" ]] || continue
|
|
type="$(cat "$supply/type" 2>/dev/null)"
|
|
[[ "$type" == "Mains" ]] || continue
|
|
found=0
|
|
online="$(cat "$supply/online" 2>/dev/null || echo 0)"
|
|
[[ "$online" == "1" ]] && return 0
|
|
done
|
|
# Mains exists and none of it is online: genuinely on battery.
|
|
(( found == 0 )) && return 1
|
|
if battery="$(battery_path)"; then
|
|
status="$(cat "$battery/status" 2>/dev/null || echo Unknown)"
|
|
[[ "$status" == "Discharging" ]] && return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# ACPI first, logind second. Some platforms expose the lid only as an evdev
|
|
# switch with no /proc/acpi/button entry; logind watches the switch either
|
|
# way, so its LidClosed property is the fallback that keeps clamshell
|
|
# detection honest there. No logind (a container, a test tree) means the
|
|
# fallback quietly answers open, which is the safe direction.
|
|
lid_closed() {
|
|
local state
|
|
for state in "$ACPI"/button/lid/*/state; do
|
|
[[ -r "$state" ]] || continue
|
|
grep -qi closed "$state" && return 0
|
|
return 1
|
|
done
|
|
[[ -d "$ACPI/button/lid" ]] && return 1
|
|
busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
|
|
org.freedesktop.login1.Manager LidClosed 2>/dev/null | grep -q 'b true'
|
|
}
|
|
|
|
# A connected output that is not the built-in panel. eDP, LVDS and DSI are the
|
|
# internal ones; everything else arrived through a cable.
|
|
has_external_monitor() {
|
|
local status connector
|
|
for status in "$SYS"/class/drm/card*-*/status; do
|
|
[[ -r "$status" ]] || continue
|
|
[[ "$(cat "$status" 2>/dev/null)" == "connected" ]] || continue
|
|
connector="$(basename "$(dirname "$status")")"
|
|
case "$connector" in
|
|
*eDP*|*LVDS*|*DSI*) continue ;;
|
|
*) return 0 ;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# The one definition the rest of the laptop work hangs on: the lid is shut and
|
|
# there is still a screen to use. Closing the lid on a dock must not suspend;
|
|
# closing it on a train must.
|
|
is_clamshell() { lid_closed && has_external_monitor; }
|
|
|
|
has_touchpad() {
|
|
local name
|
|
for name in "$SYS"/class/input/*/name; do
|
|
[[ -r "$name" ]] || continue
|
|
grep -qi touchpad "$name" && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Vendor 0x10de on a display-class device. Read from sysfs rather than lspci
|
|
# so an idle discrete GPU is not woken to answer.
|
|
has_nvidia() {
|
|
local device vendor class
|
|
for device in "$SYS"/bus/pci/devices/*; do
|
|
[[ -r "$device/vendor" && -r "$device/class" ]] || continue
|
|
vendor="$(cat "$device/vendor" 2>/dev/null)"
|
|
[[ "$vendor" == "0x10de" ]] || continue
|
|
class="$(cat "$device/class" 2>/dev/null)"
|
|
[[ "$class" == 0x03* ]] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
answer() { "$1" && printf 'true' || printf 'false'; }
|
|
|
|
cmd_json() {
|
|
printf '{"laptop":%s,"battery":%s,"ac":%s,"lidClosed":%s,"externalMonitor":%s,"clamshell":%s,"touchpad":%s,"nvidia":%s}\n' \
|
|
"$(answer is_laptop)" "$(answer has_battery)" "$(answer on_ac)" \
|
|
"$(answer lid_closed)" "$(answer has_external_monitor)" \
|
|
"$(answer is_clamshell)" "$(answer has_touchpad)" "$(answer has_nvidia)"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
laptop) is_laptop ;;
|
|
battery) has_battery ;;
|
|
battery-path) battery_path ;;
|
|
ac) on_ac ;;
|
|
lid-closed) lid_closed ;;
|
|
external-monitor) has_external_monitor ;;
|
|
clamshell) is_clamshell ;;
|
|
touchpad) has_touchpad ;;
|
|
nvidia) has_nvidia ;;
|
|
--json) cmd_json ;;
|
|
-h|--help|"")
|
|
cat <<'USAGE'
|
|
usage: panama-hw <predicate>
|
|
|
|
Exits 0 for yes, 1 for no, and prints nothing.
|
|
|
|
laptop a portable chassis
|
|
battery a battery is present
|
|
battery-path print the first battery's sysfs path (0 if found)
|
|
ac on wall power (a machine with no mains is always yes)
|
|
lid-closed the lid is shut
|
|
external-monitor a connected output that is not the built-in panel
|
|
clamshell lid shut AND an external monitor: docked, keep working
|
|
touchpad a touchpad is present
|
|
nvidia an NVIDIA display device is present
|
|
|
|
--json every answer at once
|
|
USAGE
|
|
;;
|
|
*) printf 'panama-hw: unknown predicate: %s\n' "$1" >&2; exit 2 ;;
|
|
esac
|