#!/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 <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.
#
