Files
dante_beacon/dep/dante_package/dep_support_collection.sh
2025-12-12 16:27:31 +01:00

557 lines
21 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# This script collects all the necessary information/files for support, then bundle them into a single .tgz file.
# Copyright © 2022-2025 Audinate Pty Ltd ACN 120 828 006 (Audinate). All rights reserved.
#
#
# 1. Subject to the terms and conditions of this Licence, Audinate hereby grants you a worldwide, non-exclusive,
# no-charge, royalty free licence to copy, modify, merge, publish, redistribute, sublicense, and/or sell the
# Software, provided always that the following conditions are met:
# 1.1. the Software must accompany, or be incorporated in a licensed Audinate product, solution or offering
# or be used in a product, solution or offering which requires the use of another licensed Audinate
# product, solution or offering. The Software is not for use as a standalone product without any
# reference to Audinate's products;
# 1.2. the Software is provided as part of example code and as guidance material only without any warranty
# or expectation of performance, compatibility, support, updates or security; and
# 1.3. the above copyright notice and this License must be included in all copies or substantial portions
# of the Software, and all derivative works of the Software, unless the copies or derivative works are
# solely in the form of machine-executable object code generated by the source language processor.
#
# 2. TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT.
#
# 3. TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL AUDINATE BE LIABLE ON ANY LEGAL THEORY
# (INCLUDING, WITHOUT LIMITATION, IN AN ACTION FOR BREACH OF CONTRACT, NEGLIGENCE OR OTHERWISE) FOR ANY CLAIM,
# LOSS, DAMAGES OR OTHER LIABILITY HOWSOEVER INCURRED. WITHOUT LIMITING THE SCOPE OF THE PREVIOUS SENTENCE THE
# EXCLUSION OF LIABILITY SHALL INCLUDE: LOSS OF PRODUCTION OR OPERATION TIME, LOSS, DAMAGE OR CORRUPTION OF
# DATA OR RECORDS; OR LOSS OF ANTICIPATED SAVINGS, OPPORTUNITY, REVENUE, PROFIT OR GOODWILL, OR OTHER ECONOMIC
# LOSS; OR ANY SPECIAL, INCIDENTAL, INDIRECT, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES, ARISING OUT OF OR
# IN CONNECTION WITH THIS AGREEMENT, ACCESS OF THE SOFTWARE OR ANY OTHER DEALINGS WITH THE SOFTWARE, EVEN IF
# AUDINATE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH CLAIM, LOSS, DAMAGES OR OTHER LIABILITY.
#
# 4. APPLICABLE LEGISLATION SUCH AS THE AUSTRALIAN CONSUMER LAW MAY APPLY REPRESENTATIONS, WARRANTIES, OR CONDITIONS,
# OR IMPOSES OBLIGATIONS OR LIABILITY ON AUDINATE THAT CANNOT BE EXCLUDED, RESTRICTED OR MODIFIED TO THE FULL
# EXTENT SET OUT IN THE EXPRESS TERMS OF THIS CLAUSE ABOVE "CONSUMER GUARANTEES". TO THE EXTENT THAT SUCH CONSUMER
# GUARANTEES CONTINUE TO APPLY, THEN TO THE FULL EXTENT PERMITTED BY THE APPLICABLE LEGISLATION, THE LIABILITY OF
# AUDINATE UNDER THE RELEVANT CONSUMER GUARANTEE IS LIMITED (WHERE PERMITTED AT AUDINATE'S OPTION) TO ONE OF
# FOLLOWING REMEDIES OR SUBSTANTIALLY EQUIVALENT REMEDIES:
# 4.1. THE REPLACEMENT OF THE SOFTWARE, THE SUPPLY OF EQUIVALENT SOFTWARE, OR SUPPLYING RELEVANT SERVICES AGAIN;
# 4.2. THE REPAIR OF THE SOFTWARE;
# 4.3. THE PAYMENT OF THE COST OF REPLACING THE SOFTWARE, OF ACQUIRING EQUIVALENT SOFTWARE, HAVING THE RELEVANT
# SERVICES SUPPLIED AGAIN, OR HAVING THE SOFTWARE REPAIRED.
#
# 5. This License does not grant any permissions or rights to use the trade marks (whether registered or unregistered),
# the trade names, or product names of Audinate.
#
# 6. If you choose to redistribute or sell the Software you may elect to offer support, maintenance, warranties,
# indemnities or other liability obligations or rights consistent with this License. However, you may only act on
# your own behalf and must not bind Audinate. You agree to indemnify and hold harmless Audinate, and its affiliates
# from any liability claimed or incurred by reason of your offering or accepting any additional warranty or additional
# liability.
#
# NOTE: this script is intended to be run on production systems where the dante_package/development
# directory might not be available (thus no `jq` to rely on for JSON parsing) and basic tools such as `id`
# could be missing (e.g. BusyBox).
# Any changes to the script should take this into account.
RED_COLOR="\e[01;31m"
GREEN_COLOR="\e[01;32m"
YELLOW_COLOR="\e[01;33m"
BLUE_COLOR="\e[01;34m"
END_COLOR="\e[0m"
red() { printf '%b %s %b' "$RED_COLOR" "$*" "$END_COLOR"; }
green() { printf '%b %s %b' "$GREEN_COLOR" "$*" "$END_COLOR"; }
blue() { printf '%b %s %b' "$BLUE_COLOR" "$*" "$END_COLOR"; }
yellow() { printf '%b %s %b' "$YELLOW_COLOR" "$*" "$END_COLOR"; }
logerr() { echo "[ $(red ERROR)] $1"; }
logwarn() { echo "[$(yellow WARNING)] $1"; }
loginfo() { echo "[ $(blue INFO)] $1"; }
logok() { echo "[ $(green OK)] $1"; }
fail() { exit 1; }
cmd_exists() { command -v -- "$1" >/dev/null 2>&1; }
# This function assumes that:
# - the JSON file is well-formed
# - key and value appear on the same line
# - strings are double-quoted and dont contain escaped quotes
# - assumes the key exists exactly once per line
get_json_field()
{
json_file=$1
field_name=$2
default="__NOT_FOUND__"
if [ ! -f "$json_file" ]; then
echo "error: file '$json_file' not found" >&2
exit 1
fi
# explaining each sed:
# - 's/^[^:]*://' removes everything up to and including the first colon
# - 's/ //' removes the first space character after the colon, if present
# - 's/^"//' and 's/"$//' removes leading and trailing double quotes from the value
# - 's/,[[:space:]]*$//' removes a trailing comma and any following whitespace (e.g. to handle lists)
# - 's/[[:space:]]*$//' trims any remaining trailing whitespace from the value
value=$(grep "\"$field_name\"" "$json_file" | \
sed -e 's/^[^:]*://' -e 's/ //' | \
sed -e 's/^"//' -e 's/"$//' | \
sed -e 's/,[[:space:]]*$//' | \
sed -e 's/[[:space:]]*$//' | head -n 1)
if [ -z "$value" ]; then
echo "$default"
else
echo "$value"
fi
}
# where DEP is installed, default value
DEFAULT_DEP_PATH="/opt/dep"
# where DEP logs are stored, default value
DEFAULT_LOGS_PATH="/var/log"
# where temporary files created by this script will be stored, default value
DEFAULT_TEMP_PATH="/tmp"
# where the archive created by this script will be stored, default value
DEFAULT_OUTPUT_PATH=$(pwd)
# DEP container logs can only be stored in /var/log at the moment.
CONT_LOGS="/var/log/dante_container.log"
usage() {
loginfo "Usage: $0 [OPTIONS]"
loginfo ""
loginfo "This tool collects diagnostic data to help debug issues with the DEP software."
loginfo ""
loginfo "Options:"
loginfo " -c <path> Specify the directory where DEP is installed."
loginfo " Default is '${DEFAULT_DEP_PATH}'."
loginfo " -l <path> Specify the directory where DEP stores its log files."
loginfo " Default is '${DEFAULT_LOGS_PATH}'."
loginfo " -o <path> Specify the output directory for the final archive and any temporary"
loginfo " files or directories created in the process. This directory must be"
loginfo " writable by the user executing the script."
loginfo " Default is the current directory, '${DEFAULT_OUTPUT_PATH}'"
loginfo ""
loginfo "Examples:"
loginfo ""
loginfo " $0 -c /apps/dep -l /tmp/logs"
loginfo ""
loginfo " Collects diagnostic data from a DEP installation in /apps/dep, DEP log files in"
loginfo " /tmp/logs, and stores the output in the current directory."
loginfo ""
loginfo " $0 -c /apps/dep -l /tmp/logs -o /tmp/dep_diagnostics"
loginfo ""
loginfo " Collects diagnostic data from a DEP installation in /apps/dep, DEP log files in"
loginfo " /tmp/logs, and stores the output in /tmp/dep_diagnostics."
loginfo ""
loginfo " $0 -o /home/user/dep_diagnostics"
loginfo ""
loginfo " Uses the default DEP installation and log file paths, and stores the output in"
loginfo " /home/user/dep_diagnostics."
}
# Copy a file or directory from a source to a destination.
#
# Arguments:
# src (str): the source file or directory to be copied.
# dst (str): the destination where the source will be copied.
# msg (str): an error message to be logged if the copy operation fails.
#
# Behaviour:
# If the source is a directory, the function performs a recursive
# copy. If the copy operation fails for any reason, it logs a warning
# message using the provided `msg` argument along with the captured
# error message from the failed copy operation.
#
# NOTE: the function uses `eval` to allow for correct parameter expansion
# (e.g. "cp /var/log/dante_*" wouldn't work otherwise).
copy() {
src="$1"
dst="$2"
msg="$3"
cmd="cp ${src} ${dst}"
if [ -d "${src}" ]; then
cmd="cp -r ${src} ${dst}"
fi
err=$(eval "${cmd}" 2>&1)
res=$?
if [ "${res}" -ne 0 ]; then
logwarn "$msg: $err"
fi
}
# Checks if a specified directory exists and if it's writable.
#
# Arguments:
# path (str): Directory to check.
# check_write (str): '1' to check write permission, '0' otherwise.
# err_msg (str): Optional. Additional error message to display.
#
# Behaviour:
# Logs an error and exits if `path` is not a valid directory.
# If `check_write` is '1', also checks for write permission.
# Logs an error and exits if the directory is not writable.
check_path() {
path="$1"
check_write="$2"
err_msg="$3"
_ret_val=0
if [ ! -d "${path}" ]; then
logerr "${path} is not a valid path"
_ret_val=1
elif [ ! -w "${path}" ] && [ "${check_write}" = "1" ]; then
logerr "you don't have writing permission for the directory: $path"
_ret_val=1
fi
if [ "${err_msg}" ] && [ ${_ret_val} -eq 1 ]; then
logerr "${err_msg}"
fi
if [ ${_ret_val} -eq 1 ]; then
exit ${_ret_val}
fi
}
collect_kernel_config() {
dest_path="$1"
config_file=""
is_gzipped=0
if [ -f "/proc/config.gz" ]; then
config_file="/proc/config.gz"
is_gzipped=1
elif [ -f "/boot/config-$(uname -r)" ]; then
config_file="/boot/config-$(uname -r)"
elif [ -f "/boot/config" ]; then
config_file="/boot/config"
elif [ -f "/lib/modules/$(uname -r)/build/.config" ]; then
config_file="/lib/modules/$(uname -r)/build/.config"
fi
if [ -z "$config_file" ]; then
logerr "no kernel config found in standard locations"
return
fi
loginfo "found kernel config at: $config_file"
# for gzipped config, try to decompress and copy
if [ "$is_gzipped" -eq 1 ]; then
if cmd_exists gunzip; then
if gunzip -c "$config_file" > "$dest_path"/kernel_config.txt 2>/dev/null; then
# if gunzip suceeeds, early return to avoid copy
return
fi
fi
fi
copy "$config_file" "$dest_path" "Failed to copy config from $config_file to $dest_path"
}
while getopts ":o:c:l:h" option; do
case $option in
o) # output directory
OUTPUT_PATH=$OPTARG
TEMP_PATH=$OPTARG
;;
l) # log directory
LOGS_PATH=$OPTARG
;;
c) # DEP install path
DEP_PATH=$OPTARG
;;
h) # display Help
usage
exit 0
;;
\?) # invalid option
errmsg="invalid option: -$OPTARG"
;;
:) # missing argument
errmsg="option -$OPTARG requires an argument."
;;
esac
done
# if we have an error from getopts, log it and exit
if [ -n "$errmsg" ]; then
logerr "$errmsg"
fail
fi
# if we can't create archives, we can't proceed
if ! cmd_exists tar; then
logerr "'tar' not found, unable to create archives"
fail
fi
# check whether we need to use defaults
: "${DEP_PATH:=$DEFAULT_DEP_PATH}"
: "${LOGS_PATH:=$DEFAULT_LOGS_PATH}"
: "${TEMP_PATH:=$DEFAULT_TEMP_PATH}"
: "${OUTPUT_PATH:=$DEFAULT_OUTPUT_PATH}"
# if OUTPUT_PATH can't be written to, we can't proceed
# NOTE: by checking OUTPUT_PATH we also check TEMP_PATH:
# the latter is set to /tmp by default, so it is only necessary
# to make sure we can write to it when the user has specified
# a different directory, in which case OUTPUT_PATH would have
# the same value so it makes sense to only check OUTPUT_PATH
check_path "$OUTPUT_PATH" 1 "please chose a different directory using the -o option. Try $0 -h for more information"
# check that provided paths are valid
check_path "$DEP_PATH" 0 "please chose a different directory using the -c option. Try $0 -h for more information"
check_path "$LOGS_PATH" 0 "please chose a different directory using the -l option. Try $0 -h for more information"
# this script's own log file
LOGFILE="/tmp/collector.txt"
# start logging our own output:
# - create a named pipe
# - start tee reading from it in the background
# - redirect stdout and stderr to the named pipe
# trap command ensures that the named pipe gets deleted when the script exits.
mkfifo /tmp/tmpfifo
trap 'rm /tmp/tmpfifo && rm ${LOGFILE}' EXIT
tee -a "${LOGFILE}" < /tmp/tmpfifo &
exec > /tmp/tmpfifo 2>&1
# in a world where all shells support process substitution
# this is an alternative way
# exec > >(tee -a ${LOGFILE} )
# exec 2> >(tee -a ${LOGFILE} >&2)
# output what we're running with
loginfo "DEP install path: ${DEP_PATH}"
loginfo "DEP logs path: ${LOGS_PATH}"
loginfo "Temporary files will be saved in: ${TEMP_PATH}"
loginfo "Script output archive will be saved in: ${OUTPUT_PATH}"
# we'll use a subdir to store our data
SUPPORT_DIR=${TEMP_PATH}/dep_support
# where to store the ethtool output
ETHTOOL_FILE="${SUPPORT_DIR}/ethtoolinfo.txt"
# where to store the HW clock info
HW_CLKING_FILE="${SUPPORT_DIR}/hwclk.txt"
# in case the script was interrupted midway during a previous run
rm -rf "${SUPPORT_DIR}"
# if we can't create ${SUPPORT_DIR}, we can't proceed
if ! mkdir -p "${SUPPORT_DIR}" 2>/dev/null; then
logerr "cannot create directory ${SUPPORT_DIR}: permission denied"
fail
fi
DANTE_JSON="$DEP_PATH"/dante_package/dante_data/capability/dante.json
CONFIG_JSON="$DEP_PATH"/dante_package/dante_data/capability/config.json
CONFIG_DEP="$DEP_PATH"/dante_package/dante_data/config
ACTIVATION_DIR="${DEP_PATH}/dante_package/dante_data/activation"
loginfo "Collecting config files..."
# if found, get dante.json
if [ -f "${DANTE_JSON}" ]; then
copy "${DANTE_JSON}" "${SUPPORT_DIR}" "collection of ${DANTE_JSON} failed"
else
logerr "dante.json not found in $(dirname "${DANTE_JSON}")"
fi
# if found, get config.json
if [ -f "${CONFIG_JSON}" ]; then
copy "${CONFIG_JSON}" "${SUPPORT_DIR}" "collection of ${CONFIG_JSON} failed"
else
logerr "config.json not found in $(dirname "${CONFIG_JSON}")"
fi
# if found, get all content from dante_data/config
if [ -d "${CONFIG_DEP}" ]; then
copy "${CONFIG_DEP}" "${SUPPORT_DIR}" "collection of DEP ${CONFIG_DEP} directory failed"
else
logerr "DEP config directory not found in $(dirname "${CONFIG_DEP}")"
fi
# check and collect activation files
if [ -d "${ACTIVATION_DIR}" ]; then
# copy whatever we have in the activation directory
copy "${ACTIVATION_DIR}" "${SUPPORT_DIR}" "collection of DEP activation files failed"
# log errors related to single act
for actFile in device.lic manufacturer.cert; do
if [ ! -f "${ACTIVATION_DIR}/${actFile}" ]; then
logwarn "activation file '${actFile}' not found in ${ACTIVATION_DIR}"
fi
done
else
logerr "DEP activation directory not found in $(dirname "${ACTIVATION_DIR}")"
fi
loginfo "Collecting DEP logs..."
# get all DEP logs
mkdir -p "${SUPPORT_DIR}/logs"
copy "${LOGS_PATH}/dante_*" "${SUPPORT_DIR}/logs" "collection of DEP logs failed"
# get the container logs
mkdir -p "${SUPPORT_DIR}/logs"
copy "${CONT_LOGS}" "${SUPPORT_DIR}/logs" "collection of DEP container logs failed"
loginfo "Collecting system info..."
# get kernel config
collect_kernel_config "${SUPPORT_DIR}"
# get /proc/cpuinfo
copy "/proc/cpuinfo" "${SUPPORT_DIR}/cpuinfo.txt" "collection of /proc/cpuinfo failed"
# get /proc/interrupts
copy "/proc/interrupts" "${SUPPORT_DIR}/interrupts.txt" "collection of /proc/interrupts failed"
# get mount points
mount > "${SUPPORT_DIR}/mountinfo.txt" || logwarn "collection of mount points failed"
# get info about running processes: try including thread info first,
# in case of failure (e.g. "ps" is actually BusyBox) fall back to processes only
if ! ps -efL > "${SUPPORT_DIR}/processinfo.txt" 2> /dev/null; then
ps > "${SUPPORT_DIR}/processinfo.txt" || logwarn "unable to write process info into ${SUPPORT_DIR}/processinfo.txt"
fi
# get the list of active sockets
if cmd_exists netstat; then
netstat -anp 2>/dev/null > "${SUPPORT_DIR}/netstat.txt" || logwarn "unable to collect active socket info"
else
logwarn "netstat command not available"
fi
# get info about network interfaces
if cmd_exists ip; then
ip address > "${SUPPORT_DIR}/ipinfo.txt" || logwarn "unable to write ip info to ${SUPPORT_DIR}/ipinfo.txt"
else
logwarn "ip command not available"
fi
# get ALSA version (userspace libs)
if cmd_exists aplay; then
aplay --version > "${SUPPORT_DIR}/alsa.txt" || logwarn "unable to write ALSA version to ${SUPPORT_DIR}/alsa.txt"
fi
# get kernel messages
if cmd_exists dmesg; then
dmesg > "${SUPPORT_DIR}/dmesg.txt" || logwarn "unable to collect kernel messages - dmesg failed"
fi
# get device nodes
ls -l /dev > "${SUPPORT_DIR}/device_nodes.txt" || logwarn "unable to collect info about device nodes"
# get timestamp and coalesce info about each network interface
if cmd_exists ethtool; then
for NETWORK_INTERFACE in /sys/class/net/*; do
INTERFACE_NAME=$(basename "$NETWORK_INTERFACE")
{
echo "ethtool -c \"$INTERFACE_NAME\""
ethtool -c "$INTERFACE_NAME" 2>&1
echo "------------------------"
} >> "$ETHTOOL_FILE"
{
echo "ethtool -T \"$INTERFACE_NAME\""
ethtool -T "$INTERFACE_NAME" 2>&1
echo "------------------------"
} >> "$ETHTOOL_FILE"
done
else
logwarn "ethtool command not available"
fi
# get info for HW clocking, if enabled in dante.json
if [ -f "${DANTE_JSON}" ]; then
MNT_DIR="${SUPPORT_DIR}/mnt"
ROOTFS_FILE="$DEP_PATH/dante_package/dante_data/images/0/rootfs_squash"
useHwClock=$(get_json_field "${DANTE_JSON}" useHwClock)
if [ "$useHwClock" = "true" ]; then
circuitName=$(get_json_field "${DANTE_JSON}" circuitName)
i2cBus=$(get_json_field "${DANTE_JSON}" i2cBus)
i2cAddr=$(get_json_field "${DANTE_JSON}" i2cAddr)
{
echo "circuitName=$circuitName"
echo "i2cBus=$i2cBus"
echo "i2cAddr=$i2cAddr"
} >> "$HW_CLKING_FILE"
# hwclkcfg binary is in the DEP rootfs so mount rootfs first and then run it
mkdir -p "${MNT_DIR}"
if ! mount "$ROOTFS_FILE" "${MNT_DIR}"; then
logerr "unable to collect HW clocking info: rootfs mount failed"
else
"$MNT_DIR"/dante/hwclkcfg -c --i2cbus "$i2cBus" --i2caddr "$i2cAddr" "$circuitName" >> "$HW_CLKING_FILE" 2>&1
umount "${MNT_DIR}" 2> /dev/null
fi
rm -rf "${MNT_DIR}"
fi
fi
# if we are UID 0, run dep_check.sh and save its output
if [ "$(grep -E '^Uid:' /proc/self/status | awk '{print $2}')" -eq "0" ]; then
if [ ! -f "./development/dep_check.sh" ]; then
logwarn "dep_check.sh not found, skipping"
else
loginfo "Run dep_check and collect its output..."
{ ./development/dep_check.sh "${DEP_PATH}" > "${SUPPORT_DIR}/depcheck.txt"; } 2>&1
# remove escape characters from dep_check.sh output
sed -i 's/[^[:print:]]\[[0-9;]*[a-zA-Z]//g' "${SUPPORT_DIR}/depcheck.txt"
fi
else
logwarn "could not run dep_check.sh because user was not root"
fi
# add this script own logs to the bundle
if [ -f "$LOGFILE" ]; then
# remove escape characters from this script output
sed -i 's/[^[:print:]]\[[0-9;]*[a-zA-Z]//g' "$LOGFILE"
fi
loginfo "Create final archive..."
# copy our own logs to the support directory, fail silently
cp "$LOGFILE" "${SUPPORT_DIR}/collector.txt" || true
# bundle everything together
timestamp=$(date "+%Y.%m.%d-%H.%M.%S")
tgz_name="dep_support-${timestamp}.tgz"
if ! tar czf "${OUTPUT_PATH}"/"${tgz_name}" -C "$(dirname "${SUPPORT_DIR}")" "$(basename "${SUPPORT_DIR}")" > /dev/null 2>&1; then
logerr "unable to bundle support files in ${OUTPUT_PATH}/${tgz_name}"
_exit_val=1
else
logok "DEP log files and system info bundled in ${OUTPUT_PATH}/${tgz_name}"
_exit_val=0
fi
# remove temporary data
rm -rf "${SUPPORT_DIR}"
exit ${_exit_val}
#
# Copyright © 2022-2025 Audinate Pty Ltd ACN 120 828 006 (Audinate). All rights reserved.
#