#!/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 don’t 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 Specify the directory where DEP is installed." loginfo " Default is '${DEFAULT_DEP_PATH}'." loginfo " -l Specify the directory where DEP stores its log files." loginfo " Default is '${DEFAULT_LOGS_PATH}'." loginfo " -o 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. #