#!/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 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. battery_path() { local supply type for supply in "$SYS"/class/power_supply/*; do [[ -r "$supply/type" ]] || continue type="$(cat "$supply/type" 2>/dev/null)" if [[ "$type" == "Battery" ]]; then printf '%s\n' "$supply" return 0 fi done return 1 } has_battery() { battery_path >/dev/null; } # On wall power. A machine with no mains supply at all 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. on_ac() { local supply type online found=1 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 return 0 } lid_closed() { local state for state in "$ACPI"/button/lid/*/state; do [[ -r "$state" ]] || continue grep -qi closed "$state" && return 0 done return 1 } # 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 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