#!/bin/sh # clawdie-hw-probe — single-shot JSON hardware probe # # Outputs one JSON object to stdout. All messages/errors go to stderr. # Designed to run as root for full hardware access. # Target: < 3 seconds on cold cache. # # Schema: clawdie.hw-probe.v1 set -u PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" export PATH ######################################################################## # helpers ######################################################################## die() { printf '{"error":"%s","schema_version":"clawdie.hw-probe.v1"}\n' "$(json_escape "$1")" exit 1 } json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g; s/\r//g; s/\t/\\t/g' } json_bool() { if [ "$1" = "yes" ] || [ "$1" = "YES" ] || [ "$1" = "true" ] || [ "$1" = "TRUE" ] || [ "$1" = "1" ]; then printf 'true' else printf 'false' fi } ######################################################################## # field collectors — each function prints to a temp file or variable ######################################################################## collect_hostname() { hostname 2>/dev/null || echo "unknown" } collect_freebsd_version() { # kernel, userland, runtime _k="$(freebsd-version -k 2>/dev/null || echo "unknown")" _u="$(freebsd-version -u 2>/dev/null || echo "unknown")" _r="$(freebsd-version -r 2>/dev/null || echo "unknown")" printf '{"kernel":"%s","userland":"%s","runtime":"%s"}' \ "$(json_escape "$_k")" \ "$(json_escape "$_u")" \ "$(json_escape "$_r")" } collect_ram_gb() { _bytes="$(sysctl -n hw.physmem 2>/dev/null || echo 0)" # Round to nearest GB: add 0.5 GB then truncate _half_gb=536870912 # 2^29 _gb=$(( ( _bytes + _half_gb ) / 1073741824 )) printf '%d' "$_gb" } collect_cpu_model() { sysctl -n hw.model 2>/dev/null || echo "unknown" } collect_cpu_cores() { sysctl -n hw.ncpu 2>/dev/null || echo "0" } collect_gpu() { # Extract VGA/display devices from pciconf -lv # Output: JSON array of {vendor, device, chip} _found=0 printf '[' pciconf -lv 2>/dev/null | awk ' BEGIN { in_block=0; vendor=""; device=""; chip=""; first=1 } /^[a-z]/ { if (in_block && device ~ /VGA|display/) { if (!first) printf ","; gsub(/"/, "\\\"", vendor); gsub(/"/, "\\\"", device); gsub(/"/, "\\\"", chip); printf "{\"vendor\":\"%s\",\"device\":\"%s\",\"chip\":\"%s\"}", vendor, device, chip; first=0; } in_block=1; vendor=""; device=""; chip=$0; sub(/@.*/, "", chip); } /vendor *=/ { vendor=$0; sub(/^[[:space:]]*vendor[[:space:]]*=[[:space:]]*/, "", vendor); sub(/'\''/, "", vendor); sub(/'\''$/, "", vendor) } /device *=/ { device=$0; sub(/^[[:space:]]*device[[:space:]]*=[[:space:]]*/, "", device); sub(/'\''/, "", device); sub(/'\''$/, "", device) } END { if (in_block && device ~ /VGA|display/) { if (!first) printf ","; gsub(/"/, "\\\"", vendor); gsub(/"/, "\\\"", device); gsub(/"/, "\\\"", chip); printf "{\"vendor\":\"%s\",\"device\":\"%s\",\"chip\":\"%s\"}", vendor, device, chip; } } ' printf ']' } collect_gpu_driver() { # Check which KMS drivers are loaded printf '[' _first=1 for _drv in amdgpu i915kms nvidia-modeset; do if kldstat -q -n "$_drv" 2>/dev/null; then if [ "$_first" -ne 1 ]; then printf ','; fi printf '"%s"' "$_drv" _first=0 fi done printf ']' } collect_disks() { # Disk names + sizes printf '{' _disks="$(sysctl -n kern.disks 2>/dev/null || true)" _first=1 for _disk in $_disks; do _size="" # Try geom disk list first (fast, no device open) if command -v geom >/dev/null 2>&1; then _size="$(geom disk list "$_disk" 2>/dev/null | awk '/Mediasize:/ {print $2}' | head -1)" fi # Fallback to camcontrol if [ -z "$_size" ] && command -v camcontrol >/dev/null 2>&1; then _size="$(camcontrol identify "/dev/${_disk}" 2>/dev/null | awk -F'[<>]' '/LBA48 supported|LBA supported/{print $2}' | head -1)" fi # Fallback to diskinfo if [ -z "$_size" ] && command -v diskinfo >/dev/null 2>&1; then _size="$(diskinfo "/dev/${_disk}" 2>/dev/null | awk '{print $3}' | head -1)" fi if [ "$_first" -ne 1 ]; then printf ','; fi printf '"%s":%s' "$(json_escape "$_disk")" "${_size:-0}" _first=0 done printf '}' } collect_network_interfaces() { # Simpler, reliable version that doesn't rely on subshell variable leakage _ifaces="$(ifconfig -l 2>/dev/null)" printf '{' _first=1 for _iface in $_ifaces; do _mac="$(ifconfig "$_iface" 2>/dev/null | awk '/ether/{print $2}' | head -1)" [ -z "$_mac" ] && continue if [ "$_first" -ne 1 ]; then printf ','; fi printf '"%s":"%s"' "$(json_escape "$_iface")" "$(json_escape "$_mac")" _first=0 done printf '}' } collect_wifi() { _wlan="$(sysctl -n net.wlan.devices 2>/dev/null || true)" if [ -n "$_wlan" ]; then printf '[' _first=1 for _dev in $_wlan; do if [ "$_first" -ne 1 ]; then printf ','; fi printf '"%s"' "$(json_escape "$_dev")" _first=0 done printf ']' else printf '[]' fi } collect_usb_devices() { if command -v usbconfig >/dev/null 2>&1; then printf '%d' "$(usbconfig 2>/dev/null | grep -c '^ugen' || echo 0)" else printf '0' fi } collect_xorg_running() { if pgrep -x Xorg >/dev/null 2>&1; then printf 'true' else printf 'false' fi } collect_vulkan_support() { if command -v vulkaninfo >/dev/null 2>&1 && \ vulkaninfo --summary >/dev/null 2>&1; then printf 'true' else printf 'false' fi } collect_boot_type() { if [ -e /dev/da0 ]; then printf '"usb"' elif [ -e /dev/nda0 ] || [ -e /dev/nvd0 ]; then printf '"nvme"' elif [ -e /dev/ada0 ]; then printf '"sata"' else printf '"unknown"' fi } collect_colibri_running() { if command -v service >/dev/null 2>&1; then # service status can hang on rc.d scripts; enforce a 1s deadline _status="$(timeout 1 service colibri_daemon status 2>/dev/null || echo "not_running")" case "$_status" in *"is running"*|*"Running"*) printf 'true' ;; *) printf 'false' ;; esac else printf 'false' fi } collect_zfs_pools() { if command -v zpool >/dev/null 2>&1; then _pools="$(zpool list -H -o name 2>/dev/null || true)" if [ -n "$_pools" ]; then printf '[' _first=1 for _pool in $_pools; do if [ "$_first" -ne 1 ]; then printf ','; fi printf '"%s"' "$(json_escape "$_pool")" _first=0 done printf ']' return fi fi printf '[]' } ######################################################################## # assemble JSON ######################################################################## _hostname="$(collect_hostname)" _freebsd_ver="$(collect_freebsd_version)" _ram_gb="$(collect_ram_gb)" _cpu_model="$(collect_cpu_model)" _cpu_cores="$(collect_cpu_cores)" _gpu="$(collect_gpu)" _gpu_driver="$(collect_gpu_driver)" _disks="$(collect_disks)" _net="$(collect_network_interfaces)" _wifi="$(collect_wifi)" _usb="$(collect_usb_devices)" _xorg="$(collect_xorg_running)" _vulkan="$(collect_vulkan_support)" _boot_type="$(collect_boot_type)" _colibri="$(collect_colibri_running)" _zfs_pools="$(collect_zfs_pools)" printf '{\n' printf ' "schema_version":"clawdie.hw-probe.v1",\n' printf ' "hostname":"%s",\n' "$(json_escape "$_hostname")" printf ' "freebsd_version":%s,\n' "$_freebsd_ver" printf ' "ram_gb":%s,\n' "$_ram_gb" printf ' "cpu_model":"%s",\n' "$(json_escape "$_cpu_model")" printf ' "cpu_cores":%s,\n' "$_cpu_cores" printf ' "gpu":%s,\n' "$_gpu" printf ' "gpu_driver":%s,\n' "$_gpu_driver" printf ' "disks":%s,\n' "$_disks" printf ' "network_interfaces":%s,\n' "$_net" printf ' "wifi":%s,\n' "$_wifi" printf ' "usb_devices":%s,\n' "$_usb" printf ' "xorg_running":%s,\n' "$_xorg" printf ' "vulkan_support":%s,\n' "$_vulkan" printf ' "boot_type":%s,\n' "$_boot_type" printf ' "colibri_running":%s,\n' "$_colibri" printf ' "zfs_pools":%s\n' "$_zfs_pools" printf '}\n' exit 0