|
| 1 | +#!/bin/sh |
| 2 | +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. |
| 3 | +# SPDX-License-Identifier: BSD-3-Clause-Clear |
| 4 | +# hw-probe helpers. Requires lib_common.sh, lib_apt.sh, lib_docker.sh |
| 5 | + |
| 6 | +HWPROBE_PKG="hw-probe" |
| 7 | +HWPROBE_DEPS="lshw smartmontools nvme-cli hdparm pciutils usbutils dmidecode ethtool lsscsi iproute2" |
| 8 | +# Flag for callers (e.g., run.sh) to know if we installed hw-probe in this run. |
| 9 | +# shellcheck disable=SC2034 # used by run.sh; not read within this file |
| 10 | +HWPROBE_INSTALLED_THIS_RUN=${HWPROBE_INSTALLED_THIS_RUN:-0} |
| 11 | +export HWPROBE_INSTALLED_THIS_RUN |
| 12 | + |
| 13 | +hwprobe_installed() { apt_pkg_installed "$HWPROBE_PKG"; } |
| 14 | + |
| 15 | +# Mark if we installed hw-probe in this run (for optional uninstall) |
| 16 | +HWPROBE_INSTALLED_THIS_RUN=0 |
| 17 | + |
| 18 | +hwprobe_offline_ready_local() { |
| 19 | + if ! hwprobe_installed; then |
| 20 | + return 1 |
| 21 | + fi |
| 22 | + for p in $HWPROBE_DEPS; do |
| 23 | + if ! apt_pkg_installed "$p"; then |
| 24 | + return 1 |
| 25 | + fi |
| 26 | + done |
| 27 | + return 0 |
| 28 | +} |
| 29 | + |
| 30 | +hwprobe_install_latest() { |
| 31 | + apt_ensure_deps "$HWPROBE_DEPS" || return $? |
| 32 | + if ! hwprobe_installed; then |
| 33 | + apt_install_pkgs "$HWPROBE_PKG" || return $? |
| 34 | + # shellcheck disable=SC2034 # consumed by run.sh; assignment intentional |
| 35 | + HWPROBE_INSTALLED_THIS_RUN=1 |
| 36 | + export HWPROBE_INSTALLED_THIS_RUN |
| 37 | + fi |
| 38 | + return 0 |
| 39 | +} |
| 40 | + |
| 41 | +hwprobe_install_version() { |
| 42 | + ver="$1" |
| 43 | + apt_ensure_deps "$HWPROBE_DEPS" || return $? |
| 44 | + if ! hwprobe_installed; then |
| 45 | + apt_install_pkg_version "$HWPROBE_PKG" "$ver" || return $? |
| 46 | + # shellcheck disable=SC2034 # consumed by run.sh; assignment intentional |
| 47 | + HWPROBE_INSTALLED_THIS_RUN=1 |
| 48 | + export HWPROBE_INSTALLED_THIS_RUN |
| 49 | + else |
| 50 | + apt_install_pkg_version "$HWPROBE_PKG" "$ver" || return $? |
| 51 | + fi |
| 52 | + return 0 |
| 53 | +} |
| 54 | + |
| 55 | +hwprobe_update() { apt_upgrade_pkg "$HWPROBE_PKG"; } |
| 56 | +hwprobe_list_versions() { apt_list_versions "$HWPROBE_PKG"; } |
| 57 | + |
| 58 | +hwprobe_build_local_cmd() { |
| 59 | + upload="$1"; out="$2"; extra="$3" |
| 60 | + cmd="hw-probe -all -save \"$out\"" |
| 61 | + [ "$upload" = "yes" ] && cmd="$cmd -upload" |
| 62 | + [ -n "$extra" ] && cmd="$cmd $extra" |
| 63 | + printf '%s\n' "$cmd" |
| 64 | +} |
| 65 | + |
| 66 | +_hwprobe_extract_txz() { |
| 67 | + saved="$1"; dest="$2" |
| 68 | + case "$saved" in |
| 69 | + *.txz|*.tar.xz) : ;; |
| 70 | + *) log_warn "Not a txz/tar.xz, skipping extract: $saved"; return 1 ;; |
| 71 | + esac |
| 72 | + [ -f "$saved" ] || { log_warn "Cannot extract: file not found: $saved"; return 1; } |
| 73 | + ensure_dir "$dest" || { log_warn "Cannot create extract dir: $dest"; return 1; } |
| 74 | + |
| 75 | + if tar -tJf "$saved" >/dev/null 2>&1 && tar -xJf "$saved" -C "$dest"; then |
| 76 | + log_info "Extracted to: $dest"; return 0 |
| 77 | + fi |
| 78 | + if command -v bsdtar >/dev/null 2>&1 && bsdtar -xf "$saved" -C "$dest"; then |
| 79 | + log_info "Extracted to: $dest"; return 0 |
| 80 | + fi |
| 81 | + if command -v xz >/dev/null 2>&1 && xz -dc "$saved" | tar -xf - -C "$dest"; then |
| 82 | + log_info "Extracted to: $dest"; return 0 |
| 83 | + fi |
| 84 | + log_warn "Failed to extract '$saved' (need tar -J, or bsdtar, or xz)." |
| 85 | + return 1 |
| 86 | +} |
| 87 | + |
| 88 | +hwprobe_run_local() { |
| 89 | + upload="$1"; out="$2"; extra="$3"; extract="$4" |
| 90 | + |
| 91 | + ensure_dir "$out" || return 1 |
| 92 | + |
| 93 | + if ! hwprobe_installed; then |
| 94 | + if network_is_ok; then |
| 95 | + log_warn "hw-probe not installed; installing latest..." |
| 96 | + hwprobe_install_latest || return $? |
| 97 | + else |
| 98 | + log_skip "Offline: hw-probe not installed; skipping local run." |
| 99 | + return 2 |
| 100 | + fi |
| 101 | + fi |
| 102 | + |
| 103 | + if ! need_root_or_skip; then |
| 104 | + return 2 |
| 105 | + fi |
| 106 | + |
| 107 | + cmd="$(hwprobe_build_local_cmd "$upload" "$out" "$extra")" |
| 108 | + log_info "cmd(root): $cmd" |
| 109 | + tmp="${out%/}/.hw-probe-run-$(nowstamp).log" |
| 110 | + |
| 111 | + sh -c "$SUDO $cmd" >"$tmp" 2>&1 |
| 112 | + rc=$? |
| 113 | + cat "$tmp" |
| 114 | + |
| 115 | + url="$(sed -n 's/^.*Probe URL: *\([^ ]*linux-hardware\.org[^ ]*\).*$/\1/p' "$tmp" | tail -n 1)" |
| 116 | + [ -n "$url" ] && log_info "Probe uploaded: $url" |
| 117 | + |
| 118 | + saved="$(sed -n 's/^Saved to:[[:space:]]*//p' "$tmp" | tail -n 1)" |
| 119 | + if [ -z "$saved" ] || [ ! -f "$saved" ]; then |
| 120 | + newest="$(find "$out" -mindepth 1 -maxdepth 1 -type f -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -n 1 | cut -d' ' -f2-)" |
| 121 | + [ -n "$newest" ] && saved="$newest" |
| 122 | + fi |
| 123 | + |
| 124 | + if [ "$rc" -eq 0 ] && [ -n "$saved" ] && [ -f "$saved" ]; then |
| 125 | + log_info "Latest saved artifact: $saved" |
| 126 | + log_info "List: tar -tJf \"$saved\"" |
| 127 | + log_info "Extract: mkdir -p \"$out/extracted\" && tar -xJf \"$saved\" -C \"$out/extracted\"" |
| 128 | + if [ "$extract" = "yes" ]; then |
| 129 | + dest="${out%/}/extracted-$(nowstamp)" |
| 130 | + _hwprobe_extract_txz "$saved" "$dest" || true |
| 131 | + fi |
| 132 | + fi |
| 133 | + |
| 134 | + log_info "Local report directory: $out" |
| 135 | + return "$rc" |
| 136 | +} |
| 137 | + |
| 138 | +hwprobe_run_docker() { |
| 139 | + upload="$1"; out="$2"; extra="$3"; extract="$4" |
| 140 | + |
| 141 | + ensure_dir "$out" || return 1 |
| 142 | + OUT_ABS="$(cd "$out" 2>/dev/null && pwd)" || return 1 |
| 143 | + |
| 144 | + # Track whether docker existed before, so the caller can optionally uninstall. |
| 145 | + if docker_is_installed; then export __DOCKER_WAS_INSTALLED=1; else export __DOCKER_WAS_INSTALLED=0; fi |
| 146 | + |
| 147 | + docker_install || return $? |
| 148 | + if ! docker_can_run; then |
| 149 | + log_skip "Docker present but cannot run (needs group or passwordless sudo)." |
| 150 | + return 2 |
| 151 | + fi |
| 152 | + |
| 153 | + DCMD="$(docker_cmd)" |
| 154 | + IMAGE="linuxhw/hw-probe" |
| 155 | + CNAME="hwprobe-$(nowstamp)-$$" |
| 156 | + |
| 157 | + # Ensure we have the image (best-effort pull if online) |
| 158 | + if network_is_ok; then |
| 159 | + log_info "cmd: $DCMD pull $IMAGE || true" |
| 160 | + $DCMD pull "$IMAGE" || true |
| 161 | + else |
| 162 | + if ! docker_image_exists "$IMAGE"; then |
| 163 | + log_skip "Offline and Docker image '$IMAGE' not present locally; skipping docker run." |
| 164 | + return 2 |
| 165 | + fi |
| 166 | + log_warn "Offline: skipping docker pull; using local image '$IMAGE'." |
| 167 | + fi |
| 168 | + |
| 169 | + # Build a tiny script on the HOST, inside the bind mount, to avoid quoting issues. |
| 170 | + INNER="$OUT_ABS/.inner.sh" |
| 171 | + { |
| 172 | + echo 'set -ex' |
| 173 | + echo 'echo "--- container: whoami ---"; whoami || true' |
| 174 | + echo 'echo "--- container: uname -a ---"; uname -a || true' |
| 175 | + echo 'echo "--- container: pre-touch ---"; touch /out/__pre_touch_from_container || true' |
| 176 | + echo 'echo "--- container: hw-probe -V ---"; hw-probe -V || true' |
| 177 | + echo 'if [ -f /etc/alpine-release ] && command -v apk >/dev/null 2>&1; then' |
| 178 | + echo ' apk add --no-cache kmod-libs 2>/dev/null || true' |
| 179 | + echo 'fi' |
| 180 | + printf 'echo "--- container: run hw-probe ---"; env DDCUTIL_DISABLE=1 hw-probe -all -save /out -log-level maximal' |
| 181 | + [ "$upload" = "yes" ] && printf ' -upload' |
| 182 | + [ -n "$extra" ] && printf ' %s' "$extra" |
| 183 | + echo |
| 184 | + echo 'echo "--- container: list /out ---"; ls -la /out || true' |
| 185 | + echo 'echo "--- container: post-touch ---"; touch /out/__post_touch_from_container || true' |
| 186 | + echo 'echo "--- container: done ---"' |
| 187 | + } > "$INNER" |
| 188 | + chmod 755 "$INNER" 2>/dev/null || true |
| 189 | + |
| 190 | + TS="$(nowstamp)" |
| 191 | + DLOG="${OUT_ABS%/}/.hw-probe-docker-${TS}.log" |
| 192 | + |
| 193 | + # Preview exact docker run (multi-line, readable) |
| 194 | + log_info "cmd:" |
| 195 | + log_info " $DCMD run --name $CNAME \\" |
| 196 | + log_info " --privileged --net=host --pid=host \\" |
| 197 | + log_info " -v /dev:/dev \\" |
| 198 | + log_info " -v /sys:/sys:ro \\" |
| 199 | + log_info " -v /run/udev:/run/udev:ro \\" |
| 200 | + log_info " -v /lib/modules:/lib/modules:ro \\" |
| 201 | + log_info " -v /etc/os-release:/etc/os-release:ro \\" |
| 202 | + log_info " -v /var/log:/var/log:ro \\" |
| 203 | + log_info " -v \"$OUT_ABS\":/out \\" |
| 204 | + log_info " --entrypoint /bin/sh \\" |
| 205 | + log_info " \"$IMAGE\" -lc \"/out/.inner.sh 2>&1 | tee -a /out/container.log\"" |
| 206 | + |
| 207 | + # Run (no --rm so we can inspect/cp afterwards). Capture all stdout/stderr to DLOG. |
| 208 | + ( |
| 209 | + echo "== docker run start: $(date -u) ==" |
| 210 | + $DCMD run --name "$CNAME" \ |
| 211 | + --privileged --net=host --pid=host \ |
| 212 | + -v /dev:/dev \ |
| 213 | + -v /sys:/sys:ro \ |
| 214 | + -v /run/udev:/run/udev:ro \ |
| 215 | + -v /lib/modules:/lib/modules:ro \ |
| 216 | + -v /etc/os-release:/etc/os-release:ro \ |
| 217 | + -v /var/log:/var/log:ro \ |
| 218 | + -v "$OUT_ABS":/out \ |
| 219 | + --entrypoint /bin/sh \ |
| 220 | + "$IMAGE" -lc "/out/.inner.sh 2>&1 | tee -a /out/container.log" |
| 221 | + rc=$? |
| 222 | + echo "== docker run end: $(date -u) rc=$rc ==" |
| 223 | + exit $rc |
| 224 | + ) >"$DLOG" 2>&1 |
| 225 | + |
| 226 | + run_rc=$? |
| 227 | + |
| 228 | + # Always show docker logs if available (helpful, but container.log is authoritative). |
| 229 | + log_info "--- docker logs ($CNAME) ---" |
| 230 | + $DCMD logs "$CNAME" 2>/dev/null | sed 's/^/[docker] /' || log_warn "No docker logs (possibly empty)." |
| 231 | + |
| 232 | + # Show container state |
| 233 | + log_info "--- docker inspect state ($CNAME) ---" |
| 234 | + $DCMD inspect --format='[state] Status={{.State.Status}} ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}}' "$CNAME" 2>/dev/null | sed 's/^/[inspect] /' || true |
| 235 | + |
| 236 | + # Mirror our captured host log + the container.log written via tee inside container |
| 237 | + if [ -s "$DLOG" ]; then |
| 238 | + sed -e 's/^/[runner] /' "$DLOG" || true |
| 239 | + fi |
| 240 | + if [ -s "$OUT_ABS/container.log" ]; then |
| 241 | + log_info "--- container.log (host copy) ---" |
| 242 | + sed -e 's/^/[container] /' "$OUT_ABS/container.log" | tail -n 200 || true |
| 243 | + else |
| 244 | + log_warn "container.log not found in $OUT_ABS (script may not have run)." |
| 245 | + fi |
| 246 | + |
| 247 | + # Locate artifacts on the host |
| 248 | + saved="$(find "$OUT_ABS" -maxdepth 1 -type f -name '*.txz' -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -n1 | cut -d' ' -f2-)" |
| 249 | + pre_marker="$OUT_ABS/__pre_touch_from_container" |
| 250 | + post_marker="$OUT_ABS/__post_touch_from_container" |
| 251 | + |
| 252 | + # If nothing visible on the host, try docker cp of /out |
| 253 | + if [ -z "$saved" ] && [ ! -e "$pre_marker" ] && [ ! -e "$post_marker" ]; then |
| 254 | + log_warn "No .txz and no markers on host. Trying docker cp fallback from container /out ..." |
| 255 | + TMP_EXTRACT="$OUT_ABS/.from_container_$TS" |
| 256 | + mkdir -p "$TMP_EXTRACT" 2>/dev/null || true |
| 257 | + if $DCMD cp "$CNAME":/out/. "$TMP_EXTRACT"/ >/dev/null 2>&1; then |
| 258 | + log_info "Copied /out from container to $TMP_EXTRACT" |
| 259 | + find "$TMP_EXTRACT" -mindepth 1 -maxdepth 1 -printf '%p\n' 2>/dev/null | sed 's/^/[cp] /' || true |
| 260 | + saved="$(find "$TMP_EXTRACT" -maxdepth 1 -type f -name '*.txz' -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -n1 | cut -d' ' -f2-)" |
| 261 | + [ -n "$saved" ] && { mv -f "$saved" "$OUT_ABS"/ 2>/dev/null || true; saved="$OUT_ABS/$(basename "$saved")"; } |
| 262 | + for m in "$TMP_EXTRACT"/__pre_touch_from_container "$TMP_EXTRACT"/__post_touch_from_container; do |
| 263 | + if [ -e "$m" ]; then |
| 264 | + mv -f "$m" "$OUT_ABS"/ 2>/dev/null || true |
| 265 | + fi |
| 266 | + done |
| 267 | + if [ -s "$TMP_EXTRACT/container.log" ]; then |
| 268 | + cp -f "$TMP_EXTRACT/container.log" "$OUT_ABS"/ 2>/dev/null || true |
| 269 | + fi |
| 270 | + else |
| 271 | + log_warn "docker cp failed or /out was empty." |
| 272 | + fi |
| 273 | + fi |
| 274 | + |
| 275 | + # Show marker status so we know if container could write to the mount |
| 276 | + if [ -e "$pre_marker" ] || [ -e "$post_marker" ]; then |
| 277 | + log_info "Mount sanity: pre_marker=$( [ -e "$pre_marker" ] && echo present || echo missing ), post_marker=$( [ -e "$post_marker" ] && echo present || echo missing )" |
| 278 | + fi |
| 279 | + |
| 280 | + # Parse possible Probe URL (container.log usually has it) |
| 281 | + url="" |
| 282 | + [ -f "$OUT_ABS/container.log" ] && url="$(sed -n 's/^.*Probe URL: *\([^ ]*linux-hardware\.org[^ ]*\).*$/\1/p' "$OUT_ABS/container.log" | tail -n 1)" |
| 283 | + [ -z "$url" ] && url="$(sed -n 's/^.*Probe URL: *\([^ ]*linux-hardware\.org[^ ]*\).*$/\1/p' "$DLOG" | tail -n 1)" |
| 284 | + [ -n "$url" ] && log_info "Probe uploaded: $url" |
| 285 | + |
| 286 | + # Tidy up container (image cleanup handled elsewhere if --uninstall yes) |
| 287 | + $DCMD rm -f "$CNAME" >/dev/null 2>&1 || true |
| 288 | + |
| 289 | + # Best-effort chown to current user so artifacts aren’t root-owned on host |
| 290 | + if command -v id >/dev/null 2>&1; then |
| 291 | + uid="$(id -u 2>/dev/null || echo)" |
| 292 | + gid="$(id -g 2>/dev/null || echo)" |
| 293 | + if [ -n "$uid" ] && [ -n "$gid" ]; then |
| 294 | + if need_root_or_skip; then |
| 295 | + log_info "Fixing ownership of $OUT_ABS -> $uid:$gid" |
| 296 | + sh -c "$SUDO chown -R \"$uid\":\"$gid\" \"$OUT_ABS\" 2>/dev/null || true" |
| 297 | + else |
| 298 | + log_warn "Outputs are root-owned. re-run with sudo to chown, or copy elsewhere." |
| 299 | + fi |
| 300 | + fi |
| 301 | + fi |
| 302 | + |
| 303 | + if [ -n "$saved" ] && [ -f "$saved" ]; then |
| 304 | + log_info "Latest saved artifact: $saved" |
| 305 | + log_info "List: tar -tJf \"$saved\"" |
| 306 | + log_info "Extract: mkdir -p \"$OUT_ABS/extracted\" && tar -xJf \"$saved\" -C \"$OUT_ABS/extracted\"" |
| 307 | + if [ "$extract" = "yes" ]; then |
| 308 | + dest="${OUT_ABS%/}/extracted-$(nowstamp)" |
| 309 | + _hwprobe_extract_txz "$saved" "$dest" || true |
| 310 | + fi |
| 311 | + log_info "Docker run complete. Report directory: $OUT_ABS" |
| 312 | + return 0 |
| 313 | + fi |
| 314 | + |
| 315 | + log_fail "No .txz artifact produced (docker run rc=$run_rc)." |
| 316 | + log_info "Docker run complete. Report directory: $OUT_ABS" |
| 317 | + return 1 |
| 318 | +} |
| 319 | + |
| 320 | +# ---- New: uninstall hw-probe package ---- |
| 321 | +hwprobe_uninstall_pkg() { |
| 322 | + if ! hwprobe_installed; then |
| 323 | + return 0 |
| 324 | + fi |
| 325 | + apt_remove_pkgs "$HWPROBE_PKG" || return $? |
| 326 | + apt_autoremove || true |
| 327 | + return 0 |
| 328 | +} |
| 329 | + |
| 330 | +hwprobe_uninstall() { |
| 331 | + if apt_pkg_installed "$HWPROBE_PKG"; then |
| 332 | + need_root |
| 333 | + log_info "cmd(root): apt-get remove -y $HWPROBE_PKG" |
| 334 | + sh -c "$SUDO DEBIAN_FRONTEND=noninteractive apt-get remove -y $HWPROBE_PKG" |
| 335 | + else |
| 336 | + log_info "hw-probe not installed; nothing to uninstall." |
| 337 | + fi |
| 338 | +} |
| 339 | + |
| 340 | +docker_image_prune_hwprobe() { |
| 341 | + DCMD="$(docker_cmd)" |
| 342 | + if docker_image_exists "linuxhw/hw-probe"; then |
| 343 | + log_info "Cleaning up Docker image linuxhw/hw-probe (best-effort)" |
| 344 | + # shellcheck disable=SC2086 |
| 345 | + $DCMD rmi -f linuxhw/hw-probe >/dev/null 2>&1 || true |
| 346 | + fi |
| 347 | +} |
0 commit comments