438 lines
14 KiB
Bash
438 lines
14 KiB
Bash
#!/usr/bin/env bash
|
|||
|
|
# Collect a privacy-conscious, read-only RK1 media diagnostic bundle.
|
||
|
|
|
||
|
|
set -Eeuo pipefail
|
||
|
|
export LC_ALL=C
|
||
|
|
umask 077
|
||
|
|
|
||
|
|
OUTPUT=""
|
||
|
|
DIRECTORY_OUTPUT=0
|
||
|
|
ACTIVE_TESTS=0
|
||
|
|
MEDIA_DIR=""
|
||
|
|
TEMP_DIR=""
|
||
|
|
|
||
|
|
SYS_ROOT=${RK1_SYSFS_ROOT:-/sys}
|
||
|
|
PROC_ROOT=${RK1_PROCFS_ROOT:-/proc}
|
||
|
|
DEV_ROOT=${RK1_DEV_ROOT:-/dev}
|
||
|
|
RKNN_HOME=${RKNN_HOME:-/opt/rknn/current}
|
||
|
|
|
||
|
|
usage() {
|
||
|
|
cat <<'EOF'
|
||
|
|
Usage: rk1-media-diagnostics [OPTIONS]
|
||
|
|
|
||
|
|
Options:
|
||
|
|
--output PATH Output archive or directory path.
|
||
|
|
--directory Write an unpacked directory instead of a .tar.gz.
|
||
|
|
--include-active-tests Include short Vulkan, encode, and NPU workloads.
|
||
|
|
--media-dir PATH Pass media samples to active decode/RGA tests.
|
||
|
|
-h, --help Show this help.
|
||
|
|
|
||
|
|
The bundle is local only. It deliberately excludes environment variables,
|
||
|
|
home-directory contents, SSH material, disk serials/UUIDs, MAC addresses,
|
||
|
|
IP addresses, raw EDID, and unfiltered system journals. Review it before
|
||
|
|
sharing it with anyone.
|
||
|
|
EOF
|
||
|
|
}
|
||
|
|
|
||
|
|
die() {
|
||
|
|
printf 'rk1-media-diagnostics: %s\n' "$*" >&2
|
||
|
|
exit 2
|
||
|
|
}
|
||
|
|
|
||
|
|
while (($#)); do
|
||
|
|
case "$1" in
|
||
|
|
--output)
|
||
|
|
(($# >= 2)) || die "--output requires a path"
|
||
|
|
OUTPUT=$2
|
||
|
|
shift 2
|
||
|
|
;;
|
||
|
|
--directory)
|
||
|
|
DIRECTORY_OUTPUT=1
|
||
|
|
shift
|
||
|
|
;;
|
||
|
|
--include-active-tests)
|
||
|
|
ACTIVE_TESTS=1
|
||
|
|
shift
|
||
|
|
;;
|
||
|
|
--media-dir)
|
||
|
|
(($# >= 2)) || die "--media-dir requires a path"
|
||
|
|
MEDIA_DIR=$2
|
||
|
|
shift 2
|
||
|
|
;;
|
||
|
|
-h|--help)
|
||
|
|
usage
|
||
|
|
exit 0
|
||
|
|
;;
|
||
|
|
*)
|
||
|
|
die "unknown option: $1"
|
||
|
|
;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
|
||
|
|
if [[ -n "$MEDIA_DIR" && ! -d "$MEDIA_DIR" ]]; then
|
||
|
|
die "media directory is not a directory: $MEDIA_DIR"
|
||
|
|
fi
|
||
|
|
|
||
|
|
safe_host=$(hostname 2>/dev/null || printf rk1)
|
||
|
|
safe_host=${safe_host//[^A-Za-z0-9._-]/_}
|
||
|
|
timestamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||
|
|
if [[ -z "$OUTPUT" ]]; then
|
||
|
|
if ((DIRECTORY_OUTPUT)); then
|
||
|
|
OUTPUT="$PWD/rk1-media-diagnostics-${safe_host}-${timestamp}"
|
||
|
|
else
|
||
|
|
OUTPUT="$PWD/rk1-media-diagnostics-${safe_host}-${timestamp}.tar.gz"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
output_parent=$(dirname -- "$OUTPUT")
|
||
|
|
[[ -d "$output_parent" ]] || die "output parent does not exist: $output_parent"
|
||
|
|
output_parent=$(cd -- "$output_parent" && pwd -P)
|
||
|
|
OUTPUT="$output_parent/$(basename -- "$OUTPUT")"
|
||
|
|
[[ ! -e "$OUTPUT" && ! -L "$OUTPUT" ]] || die "refusing to overwrite $OUTPUT"
|
||
|
|
|
||
|
|
TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/rk1-media-diagnostics.XXXXXXXX")
|
||
|
|
REPORT="$TEMP_DIR/report"
|
||
|
|
mkdir -p "$REPORT"
|
||
|
|
|
||
|
|
cleanup() {
|
||
|
|
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
|
||
|
|
rm -rf -- "$TEMP_DIR"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
trap cleanup EXIT
|
||
|
|
|
||
|
|
sanitize_stream() {
|
||
|
|
# Redact common MAC, IPv4, root-device, and static-IP command-line forms.
|
||
|
|
sed -E \
|
||
|
|
-e 's/([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}/<mac-redacted>/g' \
|
||
|
|
-e 's/([[:space:]=]|^)([0-9]{1,3}\.){3}[0-9]{1,3}([[:space:]\/:]|$)/\1<ip-redacted>\3/g' \
|
||
|
|
-e 's/(root=)[^[:space:]]+/\1<root-device-redacted>/g' \
|
||
|
|
-e 's/(ip=)[^[:space:]]+/\1<ip-config-redacted>/g' \
|
||
|
|
-e 's/(UUID|PARTUUID)=[A-Za-z0-9-]+/\1=<redacted>/g' \
|
||
|
|
-e 's/([Ss]erial([ _-]?[Nn]umber)?[=:][[:space:]]*)[^[:space:],;]+/\1<redacted>/g'
|
||
|
|
}
|
||
|
|
|
||
|
|
run_capture() {
|
||
|
|
local destination=$1
|
||
|
|
shift
|
||
|
|
{
|
||
|
|
printf '$'
|
||
|
|
printf ' %q' "$@"
|
||
|
|
printf '\n'
|
||
|
|
if command -v "$1" >/dev/null 2>&1; then
|
||
|
|
timeout 45s "$@" 2>&1 ||
|
||
|
|
printf '[command exited %d]\n' "$?"
|
||
|
|
else
|
||
|
|
printf '[command unavailable: %s]\n' "$1"
|
||
|
|
fi
|
||
|
|
} | sanitize_stream >"$REPORT/$destination"
|
||
|
|
}
|
||
|
|
|
||
|
|
append_command() {
|
||
|
|
local destination=$1
|
||
|
|
shift
|
||
|
|
{
|
||
|
|
printf '\n$'
|
||
|
|
printf ' %q' "$@"
|
||
|
|
printf '\n'
|
||
|
|
if command -v "$1" >/dev/null 2>&1; then
|
||
|
|
timeout 45s "$@" 2>&1 ||
|
||
|
|
printf '[command exited %d]\n' "$?"
|
||
|
|
else
|
||
|
|
printf '[command unavailable: %s]\n' "$1"
|
||
|
|
fi
|
||
|
|
} | sanitize_stream >>"$REPORT/$destination"
|
||
|
|
}
|
||
|
|
|
||
|
|
cat >"$REPORT/README.txt" <<'EOF'
|
||
|
|
RK1 media diagnostic bundle
|
||
|
|
|
||
|
|
This is a read-only snapshot for diagnosing HDMI, GPU, RKMPP VPU, RGA,
|
||
|
|
RKNPU, ALSA, Ethernet, and storage enumeration. Commands that were missing or
|
||
|
|
permission-denied are recorded rather than treated as collector failures.
|
||
|
|
|
||
|
|
The collector attempts to redact IP and MAC addresses and omits raw EDID,
|
||
|
|
disk serials/UUIDs, environment variables, home directories, SSH material,
|
||
|
|
and unfiltered journals. Review every file before sharing the bundle.
|
||
|
|
EOF
|
||
|
|
|
||
|
|
{
|
||
|
|
printf 'collected_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||
|
|
printf 'collector_version=1\n'
|
||
|
|
printf 'active_tests=%s\n' "$ACTIVE_TESTS"
|
||
|
|
printf 'kernel='
|
||
|
|
uname -srvm 2>/dev/null || true
|
||
|
|
printf 'architecture='
|
||
|
|
uname -m 2>/dev/null || true
|
||
|
|
if [[ -r /etc/os-release ]]; then
|
||
|
|
printf '\n[os-release]\n'
|
||
|
|
grep -E '^(NAME|VERSION|VERSION_ID|ID|ID_LIKE)=' /etc/os-release || true
|
||
|
|
fi
|
||
|
|
if [[ -r "$PROC_ROOT/device-tree/model" ]]; then
|
||
|
|
printf '\nboard_model='
|
||
|
|
tr -d '\000' <"$PROC_ROOT/device-tree/model" || true
|
||
|
|
printf '\n'
|
||
|
|
fi
|
||
|
|
if [[ -r "$PROC_ROOT/cmdline" ]]; then
|
||
|
|
printf '\n[kernel-command-line-redacted]\n'
|
||
|
|
sanitize_stream <"$PROC_ROOT/cmdline"
|
||
|
|
printf '\n'
|
||
|
|
fi
|
||
|
|
} >"$REPORT/system.txt"
|
||
|
|
|
||
|
|
SELFTEST=""
|
||
|
|
if command -v rk1-media-selftest >/dev/null 2>&1; then
|
||
|
|
SELFTEST=$(command -v rk1-media-selftest)
|
||
|
|
elif [[ -x "$(dirname -- "${BASH_SOURCE[0]}")/rk1-media-selftest" ]]; then
|
||
|
|
SELFTEST="$(dirname -- "${BASH_SOURCE[0]}")/rk1-media-selftest"
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [[ -n "$SELFTEST" ]]; then
|
||
|
|
selftest_args=(--json)
|
||
|
|
if ((ACTIVE_TESTS == 0)); then
|
||
|
|
selftest_args+=(--quick)
|
||
|
|
fi
|
||
|
|
if [[ -n "$MEDIA_DIR" ]]; then
|
||
|
|
selftest_args+=(--media-dir "$MEDIA_DIR")
|
||
|
|
fi
|
||
|
|
if ! timeout 600s "$SELFTEST" "${selftest_args[@]}" \
|
||
|
|
>"$REPORT/selftest.json" 2>"$REPORT/selftest.stderr"; then
|
||
|
|
printf 'self-test returned a nonzero status; inspect its JSON and stderr\n' \
|
||
|
|
>"$REPORT/selftest-status.txt"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
printf '{"error":"rk1-media-selftest is unavailable"}\n' \
|
||
|
|
>"$REPORT/selftest.json"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# DRM connector details. EDID is represented by size, hash, and decoded text,
|
||
|
|
# never by the raw binary blob.
|
||
|
|
{
|
||
|
|
shopt -s nullglob
|
||
|
|
connectors=("$SYS_ROOT"/class/drm/card*-*)
|
||
|
|
shopt -u nullglob
|
||
|
|
if ((${#connectors[@]} == 0)); then
|
||
|
|
printf 'No DRM connectors found.\n'
|
||
|
|
fi
|
||
|
|
for connector in "${connectors[@]}"; do
|
||
|
|
[[ -d "$connector" ]] || continue
|
||
|
|
printf '\n[%s]\n' "${connector##*/}"
|
||
|
|
for attribute in status enabled dpms link_status; do
|
||
|
|
if [[ -r "$connector/$attribute" ]]; then
|
||
|
|
printf '%s=' "$attribute"
|
||
|
|
tr -d '\000' <"$connector/$attribute" 2>/dev/null || true
|
||
|
|
printf '\n'
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
if [[ -r "$connector/modes" ]]; then
|
||
|
|
printf 'modes:\n'
|
||
|
|
sed 's/^/ /' "$connector/modes" 2>/dev/null || true
|
||
|
|
fi
|
||
|
|
if [[ -s "$connector/edid" ]]; then
|
||
|
|
printf 'edid_size=%s\n' "$(wc -c <"$connector/edid")"
|
||
|
|
printf 'edid_sha256=%s\n' \
|
||
|
|
"$(sha256sum "$connector/edid" | awk '{print $1}')"
|
||
|
|
if command -v edid-decode >/dev/null 2>&1; then
|
||
|
|
printf 'decoded_edid:\n'
|
||
|
|
timeout 15s edid-decode "$connector/edid" 2>&1 |
|
||
|
|
sed 's/^/ /' || true
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
} | sanitize_stream >"$REPORT/drm-connectors.txt"
|
||
|
|
|
||
|
|
run_capture drm-modetest.txt modetest -c -p
|
||
|
|
run_capture kernel-modules.txt lsmod
|
||
|
|
|
||
|
|
{
|
||
|
|
printf '[platform GPU driver bindings]\n'
|
||
|
|
for driver in panthor panfrost mali; do
|
||
|
|
directory="$SYS_ROOT/bus/platform/drivers/$driver"
|
||
|
|
[[ -d "$directory" ]] || continue
|
||
|
|
printf '%s:\n' "$driver"
|
||
|
|
find "$directory" -mindepth 1 -maxdepth 1 \( -type l -o -type d \) \
|
||
|
|
-printf ' %f\n' 2>/dev/null | sort
|
||
|
|
done
|
||
|
|
printf '\n[GPU devfreq]\n'
|
||
|
|
shopt -s nullglob
|
||
|
|
gpu_nodes=("$SYS_ROOT"/class/devfreq/*.gpu)
|
||
|
|
shopt -u nullglob
|
||
|
|
for node in "${gpu_nodes[@]}"; do
|
||
|
|
printf '%s\n' "${node##*/}"
|
||
|
|
for attribute in cur_freq min_freq max_freq governor available_frequencies; do
|
||
|
|
if [[ -r "$node/$attribute" ]]; then
|
||
|
|
printf ' %s=' "$attribute"
|
||
|
|
tr -d '\000' <"$node/$attribute" 2>/dev/null || true
|
||
|
|
printf '\n'
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
done
|
||
|
|
} >"$REPORT/gpu-sysfs.txt"
|
||
|
|
run_capture vulkan.txt vulkaninfo --summary
|
||
|
|
append_command vulkan.txt eglinfo -B
|
||
|
|
|
||
|
|
{
|
||
|
|
printf '[media and accelerator platform bindings]\n'
|
||
|
|
for pattern in 'mpp*' 'rga*' 'rockchip-rga' 'RKNPU' 'rknpu'; do
|
||
|
|
shopt -s nullglob
|
||
|
|
directories=("$SYS_ROOT"/bus/platform/drivers/$pattern)
|
||
|
|
shopt -u nullglob
|
||
|
|
for directory in "${directories[@]}"; do
|
||
|
|
[[ -d "$directory" ]] || continue
|
||
|
|
printf '\n%s:\n' "${directory##*/}"
|
||
|
|
find "$directory" -mindepth 1 -maxdepth 1 \( -type l -o -type d \) \
|
||
|
|
-printf ' %f\n' 2>/dev/null | sort
|
||
|
|
done
|
||
|
|
done
|
||
|
|
printf '\n[device nodes]\n'
|
||
|
|
shopt -s nullglob
|
||
|
|
nodes=("$DEV_ROOT"/dri/* "$DEV_ROOT"/mpp_service \
|
||
|
|
"$DEV_ROOT"/rga "$DEV_ROOT"/rknpu* "$DEV_ROOT"/mali*)
|
||
|
|
shopt -u nullglob
|
||
|
|
for node in "${nodes[@]}"; do
|
||
|
|
stat -c '%A %U:%G %t:%T %n' "$node" 2>/dev/null || true
|
||
|
|
done
|
||
|
|
} >"$REPORT/accelerators.txt"
|
||
|
|
|
||
|
|
FFMPEG=""
|
||
|
|
for candidate in /opt/rkmedia/bin/ffmpeg-rk /usr/local/bin/ffmpeg-rk; do
|
||
|
|
if [[ -x "$candidate" ]]; then
|
||
|
|
FFMPEG=$candidate
|
||
|
|
break
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
if [[ -z "$FFMPEG" ]] && command -v ffmpeg-rk >/dev/null 2>&1; then
|
||
|
|
FFMPEG=$(command -v ffmpeg-rk)
|
||
|
|
fi
|
||
|
|
if [[ -n "$FFMPEG" ]]; then
|
||
|
|
run_capture ffmpeg-rk.txt "$FFMPEG" -hide_banner -version
|
||
|
|
append_command ffmpeg-rk.txt "$FFMPEG" -hide_banner -hwaccels
|
||
|
|
{
|
||
|
|
printf '\n[Rockchip encoders, decoders, and filters]\n'
|
||
|
|
"$FFMPEG" -hide_banner -decoders 2>/dev/null |
|
||
|
|
grep -Ei 'h264|hevc|vp9|av1|rkmpp' || true
|
||
|
|
"$FFMPEG" -hide_banner -encoders 2>/dev/null |
|
||
|
|
grep -Ei 'h264|hevc|mjpeg|rkmpp' || true
|
||
|
|
"$FFMPEG" -hide_banner -filters 2>/dev/null |
|
||
|
|
grep -Ei 'rkrga|drm' || true
|
||
|
|
} >>"$REPORT/ffmpeg-rk.txt"
|
||
|
|
else
|
||
|
|
printf 'ffmpeg-rk is unavailable\n' >"$REPORT/ffmpeg-rk.txt"
|
||
|
|
fi
|
||
|
|
|
||
|
|
{
|
||
|
|
printf '[RKNN installation]\n'
|
||
|
|
if [[ -r "$RKNN_HOME/manifest.env" ]]; then
|
||
|
|
sed -E 's#(REPOSITORY=).*#\1<upstream-url>#' "$RKNN_HOME/manifest.env"
|
||
|
|
else
|
||
|
|
printf 'manifest unavailable at %s\n' "$RKNN_HOME/manifest.env"
|
||
|
|
fi
|
||
|
|
for asset in \
|
||
|
|
"$RKNN_HOME/lib/librknnrt.so" \
|
||
|
|
"$RKNN_HOME/share/models/rk3588/mobilenet_v1.rknn" \
|
||
|
|
"$RKNN_HOME/bin/rknn-inference-test"; do
|
||
|
|
if [[ -r "$asset" ]]; then
|
||
|
|
sha256sum "$asset"
|
||
|
|
else
|
||
|
|
printf 'missing: %s\n' "$asset"
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
printf '\n[NPU devfreq]\n'
|
||
|
|
shopt -s nullglob
|
||
|
|
npu_nodes=("$SYS_ROOT"/class/devfreq/*.npu)
|
||
|
|
shopt -u nullglob
|
||
|
|
for node in "${npu_nodes[@]}"; do
|
||
|
|
printf '%s\n' "${node##*/}"
|
||
|
|
for attribute in cur_freq min_freq max_freq governor available_frequencies; do
|
||
|
|
if [[ -r "$node/$attribute" ]]; then
|
||
|
|
printf ' %s=' "$attribute"
|
||
|
|
tr -d '\000' <"$node/$attribute" 2>/dev/null || true
|
||
|
|
printf '\n'
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
done
|
||
|
|
} >"$REPORT/npu.txt"
|
||
|
|
|
||
|
|
{
|
||
|
|
printf '[ALSA cards]\n'
|
||
|
|
if [[ -r "$PROC_ROOT/asound/cards" ]]; then
|
||
|
|
cat "$PROC_ROOT/asound/cards"
|
||
|
|
else
|
||
|
|
printf 'unavailable\n'
|
||
|
|
fi
|
||
|
|
} >"$REPORT/audio.txt"
|
||
|
|
append_command audio.txt aplay -l
|
||
|
|
append_command audio.txt aplay -L
|
||
|
|
|
||
|
|
{
|
||
|
|
printf 'Interface state only; addresses and MACs are intentionally omitted.\n\n'
|
||
|
|
shopt -s nullglob
|
||
|
|
interfaces=("$SYS_ROOT"/class/net/*)
|
||
|
|
shopt -u nullglob
|
||
|
|
for interface in "${interfaces[@]}"; do
|
||
|
|
name=${interface##*/}
|
||
|
|
[[ "$name" == lo ]] && continue
|
||
|
|
printf '[%s]\n' "$name"
|
||
|
|
for attribute in operstate carrier speed duplex mtu; do
|
||
|
|
if [[ -r "$interface/$attribute" ]]; then
|
||
|
|
printf '%s=' "$attribute"
|
||
|
|
tr -d '\000' <"$interface/$attribute" 2>/dev/null || true
|
||
|
|
printf '\n'
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
printf '\n'
|
||
|
|
done
|
||
|
|
} >"$REPORT/network.txt"
|
||
|
|
|
||
|
|
run_capture storage.txt lsblk -o NAME,TYPE,SIZE,FSTYPE,MOUNTPOINTS,ROTA,TRAN
|
||
|
|
{
|
||
|
|
printf '\n[root filesystem]\n'
|
||
|
|
if command -v findmnt >/dev/null 2>&1; then
|
||
|
|
findmnt -n -o SOURCE,FSTYPE / 2>&1 | sanitize_stream
|
||
|
|
else
|
||
|
|
printf 'findmnt unavailable\n'
|
||
|
|
fi
|
||
|
|
} >>"$REPORT/storage.txt"
|
||
|
|
run_capture usb.txt lsusb -t
|
||
|
|
append_command usb.txt lspci -nnk
|
||
|
|
|
||
|
|
{
|
||
|
|
if command -v dpkg-query >/dev/null 2>&1; then
|
||
|
|
dpkg-query -W -f='${binary:Package}\t${Version}\n' 2>/dev/null |
|
||
|
|
grep -Ei 'armbian|linux-image|linux-dtb|mesa|vulkan|kodi|ffmpeg|mpp|rga|rknn|alsa|libcec' |
|
||
|
|
sort || true
|
||
|
|
elif command -v rpm >/dev/null 2>&1; then
|
||
|
|
rpm -qa 2>/dev/null |
|
||
|
|
grep -Ei 'kernel|mesa|vulkan|kodi|ffmpeg|mpp|rga|rknn|alsa|libcec' |
|
||
|
|
sort || true
|
||
|
|
else
|
||
|
|
printf 'supported package inventory tool unavailable\n'
|
||
|
|
fi
|
||
|
|
} >"$REPORT/packages.txt"
|
||
|
|
|
||
|
|
kernel_pattern='drm|hdmi|vop|edid|panthor|panfrost|mali|rknpu|npu|rkvdec|rkvenc|av1|mpp|rga|alsa|snd|cec|pcie|nvme|mmc'
|
||
|
|
{
|
||
|
|
printf '[filtered current-boot kernel messages]\n'
|
||
|
|
if command -v journalctl >/dev/null 2>&1; then
|
||
|
|
timeout 45s journalctl -b -k --no-pager 2>&1 |
|
||
|
|
grep -Ei "$kernel_pattern" | tail -2500 || true
|
||
|
|
elif command -v dmesg >/dev/null 2>&1; then
|
||
|
|
dmesg --color=never 2>&1 |
|
||
|
|
grep -Ei "$kernel_pattern" | tail -2500 || true
|
||
|
|
else
|
||
|
|
printf 'kernel log reader unavailable\n'
|
||
|
|
fi
|
||
|
|
} | sanitize_stream >"$REPORT/kernel-media.log"
|
||
|
|
|
||
|
|
if ((DIRECTORY_OUTPUT)); then
|
||
|
|
mkdir -- "$OUTPUT"
|
||
|
|
cp -a "$REPORT/." "$OUTPUT/"
|
||
|
|
chmod -R go-rwx "$OUTPUT"
|
||
|
|
else
|
||
|
|
command -v tar >/dev/null 2>&1 || die "tar is required for archive output"
|
||
|
|
tar -C "$REPORT" -czf "$OUTPUT" .
|
||
|
|
chmod 0600 "$OUTPUT"
|
||
|
|
fi
|
||
|
|
|
||
|
|
printf 'Diagnostic bundle written to %s\n' "$OUTPUT"
|