#!/bin/sh

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

# shellcheck disable=SC1091
if [ -f "$(pwd)"/dep_check_platform.sh ]; then
    . "$(pwd)"/dep_check_platform.sh
elif [ -f ./development/dep_check_platform.sh ]; then
    . ./development/dep_check_platform.sh
fi

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"; }

USER=$(whoami)
DEP_PATH="$1"
PATH_TO_BUNDLE_DIR="${DEP_PATH}"/bundle
DANTE_JSON="${DEP_PATH}"/dante_data/capability/dante.json
CONFIG_JSON="${DEP_PATH}"/dante_data/capability/config.json
DEFER_COA_CHECK=0
TAB="            "

# To use host-provided jq, set JQ to the absolute path of the jq binary.
JQ="${DEP_PATH}"/development/tools/jq

# If a different container runtime is used, its absolute path must be set here.
# When the container runtime is changed to something other than crun, please disable
# check_container_runtime_glibc() from running as it targets the specific binary
# provided by Audinate.
CONTAINER_RUNTIME=$DEP_PATH/crun

PTP_TEST="${DEP_PATH}"/development/tools/ptp_timestamping_test
PTP_TEST_README="${DEP_PATH}"/development/tools/README_ptp.txt

# 2.28 was obtained using:
#
#  $ objdump -T /usr/bin/crun | grep GLIBC_ | sed 's/.*GLIBC_\([.0-9]*\).*/\1/g' | sort -Vu | tail -n1
#
# the general rule is that the system's glibc version must be equal to or greater than the
# version required by crun, otherwise crun will not be able to start
REQUIRED_GLIBC_CRUN="2.28"
REQUIRED_GLIBC_PTP_TEST="2.34"

### functions

fail() { exit 1; }

cmd_exists() { command -v -- "$1" >/dev/null 2>&1; }

# Argument validation for JSON utility functions.
validate_json_args() {
    jsonData="$1"
    field="$2"

    if [ -z "$jsonData" ] || [ -z "$field" ]; then
        logerr "invalid use of validate_json_args (or calling function): missing arguments"
        fail
    fi
}

# Extracts a field from JSON data using jq.
#
# Arguments:
#   $1 - JSON data as a string
#   $2 - absolute path of the field to extract (e.g., ".field.subfield")
#
# If the field does not exist or is not valid JSON, it returns "null".
# While such return value represents an obvious limitation (e.g. it cannot
# distinguish between a missing field and a field with the value "null"), it
# is the best we can do given the constraints of the JSON format.
get_json_field() {
    validate_json_args "$1" "$2"
    echo "$1" | "$JQ" -erc "$2" 2>/dev/null
}

get_dante_json_field() {
    field="$1"

    if [ -z "$DANTE_JSON_DATA" ]; then
        logerr "get_dante_json_field: DANTE_JSON_DATA is empty, this function must be called after load_config_files()"
        fail
    fi

    get_json_field "$DANTE_JSON_DATA" "$field"
}

get_config_json_field() {
    field="$1"

    if [ -z "$CONFIG_JSON_DATA" ]; then
        logerr "get_config_json_field: CONFIG_JSON_DATA is empty, this function must be called after load_config_files()"
        fail
    fi

    get_json_field "$CONFIG_JSON_DATA" "$field"
}

# NOTE: checking whether the field exists and is an array is left to the caller
get_json_array_length() {
    validate_json_args "$1" "$2"

    # output the length of the array
    echo "$1" | "$JQ" "$2 | length"
}

# Extract array elements from JSON data as space-separated values.
get_json_array_elements() {
    validate_json_args "$1" "$2"

    # check if the field exists and is an array
    if is_json_field_array "$1" "$2"; then
        # output each array element as a compact JSON string, space-separated
        echo "$1" | "$JQ" -cr "$2[]? // empty" | tr '\n' ' ' | sed 's/ $//'
    else
        # field doesn't exist, isn't an array, or is null
        echo ""
    fi
}

get_dante_json_array_elements() {
    field="$1"

    if [ -z "$DANTE_JSON_DATA" ]; then
        logerr "get_dante_json_array_elements: DANTE_JSON_DATA is empty, this function must be called after load_config_files()"
        fail
    fi

    get_json_array_elements "$DANTE_JSON_DATA" "$field"
}

get_config_json_array_elements() {
    field="$1"

    if [ -z "$CONFIG_JSON_DATA" ]; then
        logerr "get_config_json_array_elements: CONFIG_JSON_DATA is empty, this function must be called after load_config_files()"
        fail
    fi

    get_json_array_elements "$CONFIG_JSON_DATA" "$field"
}

is_json_field_array() {
    validate_json_args "$1" "$2"
    echo "$1" | "$JQ" -e "$2 | type == \"array\"" >/dev/null 2>&1
}

is_json_field_object() {
    validate_json_args "$1" "$2"
    echo "$1" | "$JQ" -e "$2 | type == \"object\"" >/dev/null 2>&1
}

is_json_field_string() {
    validate_json_args "$1" "$2"
    echo "$1" | "$JQ" -e "$2 | type == \"string\"" >/dev/null 2>&1
}

is_json_field_number() {
    validate_json_args "$1" "$2"
    echo "$1" | "$JQ" -e "$2 | type == \"number\"" >/dev/null 2>&1
}

is_json_field_bool() {
    validate_json_args "$1" "$2"
    echo "$1" | "$JQ" -e "$2 | type == \"boolean\"" >/dev/null 2>&1
}

is_json_field_null() {
    validate_json_args "$1" "$2"
    echo "$1" | "$JQ" -e "$2 | type == \"null\"" >/dev/null 2>&1
}

json_field_exists() {
    validate_json_args "$1" "$2"
    echo "$1" | "$JQ" -e "$2" >/dev/null 2>&1
}

get_json_field_type() {
    validate_json_args "$1" "$2"
    echo "$1" | "$JQ" -r "$2 | type" 2>/dev/null
}

kernel_info() {
    configFile=""
    isGzipped=0

    # find kernel config file in order of most common to least common locations
    if [ -f "/proc/config.gz" ]; then
        configFile="/proc/config.gz"
        isGzipped=1
    elif [ -f "/boot/config-$(uname -r)" ]; then
        configFile="/boot/config-$(uname -r)"
    elif [ -f "/boot/config" ]; then
        configFile="/boot/config"
    elif [ -f "/lib/modules/$(uname -r)/build/.config" ]; then
        configFile="/lib/modules/$(uname -r)/build/.config"
    fi

    # no config file found
    if [ -z "$configFile" ]; then
        echo ""
        return
    fi

    if [ "$isGzipped" -eq 1 ]; then
        if cmd_exists gunzip; then
            gunzip -c "$configFile" 2>/dev/null
        else
            echo ""
        fi
    else
        cat "$configFile"
    fi
}

check_iface_coalesce() {
    iface="$1"
    coalesceErr=0

    if ! cmd_exists ethtool; then
        logwarn "'ethtool' not found, skipping coalescing check for $iface"
        return
    fi

    if ! ethtool -c "$iface" >/dev/null 2>&1; then
        logwarn "network coalescing cannot be set/read on $iface"
        return
    fi

    for dir in rx tx; do
        val=$(ethtool -c "$iface" 2>/dev/null | awk "/${dir}-usecs:/ {print \$2}")
        if [ "$val" != 1 ] && [ "$val" != "n/a" ]; then
            logwarn "$iface has ${dir}-usecs=$val, set to 1 for best performance."
            echo "${TAB}This can be set with 'ethtool -C $iface $dir-usecs 1' before starting DEP."
            echo "${TAB}Some drivers might not support this setting or only allow values significantly higher than 1 usecs."
            echo "${TAB}Please contact your network driver vendor for more information."
            coalesceErr=1
        fi
    done

    [ "$coalesceErr" -eq 0 ] && logok "$iface coalescing settings ok"
}

# NOTE: on some systems 'id' might not be available, hence we check manually
check_uid() {
    if [ "$(grep -E '^Uid:' /proc/self/status | awk '{print $2}')" -ne "0" ]; then
        logerr "this script must be executed with root privileges"
        fail
    fi
}

check_arg() {
    if [ -z "$1" ]; then
        logerr "missing argument: DEP installation path"
        loginfo "usage:"
        loginfo "    sudo ./dep_check.sh <DEP installation path>"
        fail
    fi

    if [ "${1#/}" = "$1" ]; then
        logerr "$1 is not an absolute path"
        fail
    fi

    if [ ! -d "$1" ]; then
        logerr "$1: directory not found"
        fail
    fi
}

check_runtime_deps() {
    if [ ! -f "$JQ" ]; then
        logerr "$JQ not found. This script requires jq to parse JSON files."
        echo "${TAB}Please install jq or set JQ to the absolute path of the jq binary."
        fail
    fi

    # determine installed glibc version
    if cmd_exists ldd; then
        installedGlibc=$(ldd --version 2>/dev/null | awk 'NR==1{print $NF}')
    elif [ -x /lib/libc.so.6 ]; then
        installedGlibc=$(/lib/libc.so.6 2>/dev/null | awk 'NR==1{print $NF}')
    elif [ -x /lib64/libc.so.6 ]; then
        installedGlibc=$(/lib64/libc.so.6 2>/dev/null | awk 'NR==1{print $NF}')
    else
        logwarn "could not determine installed glibc version:"
        return
    fi

    # remove trailing dot if present
    installedGlibc="${installedGlibc%.}"

    is_glibc_version_host_compatible() {
        requiredVersion="$1"

        requiredMajor=$(echo "$requiredVersion" | cut -d'.' -f1 | tr -dc '0-9')
        requiredMinor=$(echo "$requiredVersion" | cut -d'.' -f2 | tr -dc '0-9')

        installedMajor=$(echo "$installedGlibc" | cut -d'.' -f1 | tr -dc '0-9')
        installedMinor=$(echo "$installedGlibc" | cut -d'.' -f2 | tr -dc '0-9')

        if [ "$installedMajor" -lt "$requiredMajor" ] || { [ "$installedMajor" -eq "$requiredMajor" ] && [ "$installedMinor" -lt "$requiredMinor" ]; }; then
            return 0
        fi
        return 1
    }

    if ! is_glibc_version_host_compatible $REQUIRED_GLIBC_CRUN; then
        logok "host-provided glibc ($installedGlibc) is compatible with container crun (requires >= $REQUIRED_GLIBC_CRUN)"
    else
        logwarn "host-provided glibc ($installedGlibc) is older than required ($REQUIRED_GLIBC_CRUN) for container crun"
        echo "${TAB}if unable to start DEP due to crun issues, consider compiling crun"
        echo "${TAB}(or an alternative OCI-compliant container runtime) using a compatible"
        echo "${TAB}toolchain for this system"
    fi

    PTP_TEST_COMPATIBLE=0
    is_glibc_version_host_compatible "$REQUIRED_GLIBC_PTP_TEST" || PTP_TEST_COMPATIBLE=1
}

check_architecture() {
    if ! cmd_exists hexdump; then
        logwarn "host architecture check skipped: 'hexdump' not found"
        return
    fi

    if [ ! -f "$CONTAINER_RUNTIME" ]; then
        logerr "container runtime binary not found: $CONTAINER_RUNTIME"
        echo "${TAB}If you have changed the default container runtime shipped with DEP, please"
        echo "${TAB}set CONTAINER_RUNTIME to the absolute path of the new binary or just ignore this message."
        return
    fi

    HOST_BINARY=$(which uname)
    ELF_ISA_OFFSET=0x12
    ELF_ISA_SIZE=2
    ELF_ISA_ARM_32=28
    ELF_ISA_X86_64=3e
    ELF_ISA_ARM_64=b7
    # get the hexdecimal value of the architecture in the ELF header section
    HOST_ARCH=$(hexdump -e '"%x"' -s $ELF_ISA_OFFSET -n $ELF_ISA_SIZE "$HOST_BINARY")
    DEP_ARCH=$(hexdump -e '"%x"' -s $ELF_ISA_OFFSET -n $ELF_ISA_SIZE "$CONTAINER_RUNTIME")

    DEP_ARCH_NAME=
    case $DEP_ARCH in
        "$ELF_ISA_ARM_32" )
            DEP_ARCH_NAME="arm32";;
        "$ELF_ISA_X86_64" )
            DEP_ARCH_NAME="x86_64";;
        "$ELF_ISA_ARM_64" )
            DEP_ARCH_NAME="arm64";;
        * )
            DEP_ARCH_NAME="ELF_ISA: $DEP_ARCH";;
    esac

    HOST_ARCH_NAME=
    case $HOST_ARCH in
        "$ELF_ISA_ARM_32" )
            HOST_ARCH_NAME="arm32";;
        "$ELF_ISA_X86_64" )
            HOST_ARCH_NAME="x86_64";;
        "$ELF_ISA_ARM_64" )
            HOST_ARCH_NAME="arm64";;
        * )
            HOST_ARCH_NAME="ELF_ISA: $HOST_ARCH";;
    esac

    if [ "$HOST_ARCH" = "$DEP_ARCH" ]; then
        logok "DEP and host architecture matched"
        return
    else
        if [ "$HOST_ARCH" = "$ELF_ISA_ARM_64" ] && [ "$DEP_ARCH" = "$ELF_ISA_ARM_32" ]; then
            # execute the container runtime without arguments to check whether the host is
            # available for multiple architecture supports suppressing both output and error
            # messages of the container runtime
            # shellcheck disable=SC2086
            ./${CONTAINER_RUNTIME} > /dev/null 2>&1
            CHECK_MULTI_ARCH_SUPPORT=$?
            if [ "$CHECK_MULTI_ARCH_SUPPORT" -eq 0 ]; then
                logerr "it appears you are running DEP built for arm32 on an arm64 host. Please obtain DEP built for arm64 instead"
            else
                logerr "system incompatibility detected: host CPU is $HOST_ARCH_NAME while DEP was built for $DEP_ARCH_NAME"
            fi
        else
            logerr "system incompatibility detected: host CPU is $HOST_ARCH_NAME while DEP was built for $DEP_ARCH_NAME"
        fi
    fi
}

check_kernel_config() {
    kernelConfig=$(kernel_info)
    if [ -z "$kernelConfig" ]; then
        logwarn "kernel config not found, skipping kernel config checks"
        return
    fi
    
    # Check if a kernel config option is enabled, built as module, or disabled
    # Usage: check_config_option CONFIG_NAME [required|disabled]
    check_config_option() {
        configName="$1"
        requirement="${2:-optional}"

        enabledCount=$(echo "$kernelConfig" | grep -cw "${configName}=y")
        moduleCount=$(echo "$kernelConfig" | grep -cw "${configName}=m")

        # handle options that should be disabled
        if [ "$requirement" = "disabled" ]; then
            if [ "$enabledCount" -ne 0 ] || [ "$moduleCount" -ne 0 ]; then
                logwarn "$configName needs to be disabled in kernel config"
            fi
            return 1
        fi

        # handle enabled/module options
        if [ "$enabledCount" -eq 1 ]; then
            return 0
        fi

        if [ "$moduleCount" -eq 1 ]; then
            return 0
        fi

        # option not found
        if [ "$requirement" = "required" ]; then
            logerr "$configName not found in kernel config"
        else
            logwarn "$configName not found in kernel config"
        fi
        return 1
    }

    # Define required kernel configuration options
    requiredConfigs="CGROUPS CGROUP_DEVICE CPUSETS CGROUP_SCHED MEMCG NAMESPACES UTS_NS IPC_NS USER_NS PID_NS NET_NS SQUASHFS SQUASHFS_ZLIB BLK_DEV_LOOP IP_MULTICAST"
    
    # check required kernel configs
    for option in $requiredConfigs; do
        check_config_option "CONFIG_$option" "required"
    done

    # check recommended kernel configs
    check_config_option "CONFIG_PREEMPT" "optional"
    check_config_option "CONFIG_CGROUP_FREEZER" "optional"

    # DEP doesn't support fine-grained real-time scheduling per cgroup yet.
    # Hence, CONFIG_RT_GROUP_SCHED needs to be disabled to start DEP
    # TODO: is this actually true?
    check_config_option "CONFIG_RT_GROUP_SCHED" "disabled"
}

check_cgroups() {
    cgroupWarningMsg="one or more cgroup mounts were not found and DEP is configured to run with
            numDepCores = 0 in dante.json: if this is intended, please ignore this warning."
    cgroupMountErr=0

    display_notfound() { logwarn "cgroup mount '$1' not found"; }
    display_found() { logok "cgroup mount '$1' found"; }

    check_mount() {
        path=$1
        expectedMountType=$2

        while IFS= read -r line; do
            # get the second field (mount point) and the third field (type)
            mountPoint=$(echo "$line" | awk '{print $2}')
            mountType=$(echo "$line" | awk '{print $3}')

            # if the mountPoint doesn't start with /sys/fs/cgroup, skip it
            case "$mountPoint" in
                /sys/fs/cgroup*) ;;
                *) continue ;;
            esac

            # if mount point and type exactly match the expected values, we're good
            if [ "$mountPoint" = "$path" ] && [ "$mountType" = "$expectedMountType" ]; then
                display_found "$path ($expectedMountType)"
                return
            fi

            # There is a chance multiple controllers are mounted on the same path,
            # for instance we might be looking for /sys/fs/cgroup/cpu and have
            #
            #   /sys/fs/cgroup/cpu,cpuacct cgroup etc..
            #
            # mounted instead.

            # because we skip entries that do not start with /sys/fs/cgroup at
            # the beginning of the loop, we know getting the substring after
            # /sys/fs/cgroup at this point will yield an empty string at worst
            cgroupSubstring=${mountPoint#/sys/fs/cgroup/}

            # do the same with $path
            cgroupPathSubstring=${path#/sys/fs/cgroup/}

            # check if cgroupPathSubstring is part of cgroupSubstring
            # eg this would successfully match 'cpu' against both 'cpuacct,cpu' and 'cpu,cpuacct'
            if echo "$cgroupSubstring" | grep -qw "$cgroupPathSubstring"; then
                if echo "$mountType" | grep -qw "$expectedMountType"; then
                    display_found "$path ($expectedMountType)"
                    return
                fi
            fi
        done < /proc/mounts

        display_notfound "$path (expected type: $expectedMountType)"
        cgroupMountErr=1
    }

    # hybrid cgroup mode detection
    v1Found=$(awk '$3 == "cgroup" { found=1 } END { print found+0 }' /proc/mounts)
    v2Found=$(awk '$3 == "cgroup2" { found=1 } END { print found+0 }' /proc/mounts)

    if [ "$v1Found" -eq 1 ] && [ "$v2Found" -eq 1 ]; then
        logwarn "Hybrid cgroup mode detected: both cgroup v1 and v2 are mounted."
        echo "${TAB}The default crun runtime shipped with DEP does not support hybrid cgroup mode."
        echo "${TAB}Please ensure only one cgroup version (v1 or v2) is in use on your system."
        echo "${TAB}If DEP fails to start, refer to the DEP manual for more information on supported cgroup configurations."
        return
    fi

    if ! json_field_exists "$DANTE_JSON_DATA" ".platform.cgroupVersion"; then
        logwarn "'cgroupVersion' not set in dante.json, assuming cgroup v1 (legacy)"
        cgroupVersion="1"
    else
        cgroupVersion=$(get_dante_json_field ".platform.cgroupVersion")
    fi

    if [ "$cgroupVersion" = "1" ]; then
        selectedVersion="v1"
        check_mount "/sys/fs/cgroup" "tmpfs"
        check_mount "/sys/fs/cgroup/cpu" "cgroup"
        check_mount "/sys/fs/cgroup/cpuset" "cgroup"
        check_mount "/sys/fs/cgroup/memory" "cgroup"
        check_mount "/sys/fs/cgroup/devices" "cgroup"
    elif [ "$cgroupVersion" = "2" ]; then
        selectedVersion="v2"
        check_mount "/sys/fs/cgroup" "cgroup2"
    else
        logerr "unsupported cgroupVersion value ($cgroupVersion) in $DANTE_JSON"
        return
    fi

    if [ "$cgroupMountErr" -eq 1 ]; then
        if is_json_field_number "$DANTE_JSON_DATA" ".audio.numDepCores"; then
            depCores=$(get_dante_json_field ".audio.numDepCores")
        else
            # If numDepCores is not a number, assume it's a non-zero configuration
            # (e.g., an array of specific cores). We only need to distinguish
            # between 0 (disabled) and non-zero (enabled) for cgroup error handling.
            depCores=1
        fi

        if [ "$depCores" -ne 0 ]; then
            logerr "one or more required cgroup mounts were not found"
        else
            logwarn "$cgroupWarningMsg"
        fi
    else
        logok "all required cgroup ${selectedVersion} mounts found"
    fi
}

check_cpu_config() {
    configuredCores=""
    checkFailed=0
    depProcesses="dep_manager apec DepApe conmon_server mdnsd ptp hwclkcfg DepClockBridge"

    loginfo "validating CPU configuration and runtime affinity settings"

    # utility to check that given a string that string is found in depProcesses
    is_dep_process() {
        for depProc in $depProcesses; do
            if [ "$1" = "$depProc" ]; then
                return 0
            fi
        done
        return 1
    }

    if json_field_exists "$DANTE_JSON_DATA" ".audio.percentCpuShare"; then
        logwarn "the percentCpuShare option in $DANTE_JSON was deprecated in DEP 1.5 and can be removed"
    fi

    if is_json_field_number "$DANTE_JSON_DATA" ".audio.numDepCores"; then
        numCores=$(get_dante_json_field ".audio.numDepCores")

        if [ "$numCores" -eq 0 ]; then
            loginfo "numDepCores is set to 0, DEP won't perform CPU affinity allocation - skipping check"
            return
        fi

        # generate space-separated core IDs from 0 to numCores-1
        i=0
        while [ "$i" -lt "$numCores" ]; do
            if [ -z "$configuredCores" ]; then
                configuredCores="$i"
            else
                configuredCores="$configuredCores $i"
            fi
            i=$((i + 1))
        done

    elif is_json_field_array "$DANTE_JSON_DATA" ".audio.numDepCores"; then
        configuredCores=$(get_dante_json_array_elements ".audio.numDepCores")
        if [ -z "$configuredCores" ]; then
            logerr "numDepCores array is empty in $DANTE_JSON"
            return
        fi

    else
        logerr "numDepCores is not a number or an array in $DANTE_JSON"
        return
    fi

    # check that each CPU ID specified in configuredCores actually exists on the system
    for core in $configuredCores; do
        # check CPU ID validity
        if ! printf '%s\n' "$core" | grep -Eq '^[0-9]+$'; then
            logerr "invalid core ID '$core' in numDepCores - must be a non-negative integer"
            checkFailed=1
            continue
        fi

        # check if the core is online (exists in /sys/devices/system/cpu/cpu$core)
        if [ ! -d "/sys/devices/system/cpu/cpu$core" ]; then
            logerr "core ID '$core' in numDepCores does not exist on the system"
            checkFailed=1
        fi
    done

    # before we start checking processes that might interfere with DEP, we need
    # to do some self-examination and collect all the PIDs from 1 (init) to our own PID
    currentPid=$$
    currentPidTree=""
    while [ "$currentPid" -ne 1 ] && [ -n "$currentPid" ]; do
        currentPidTree="$currentPid $currentPidTree"
        # try to get the parent PID, but if the process no longer exists, break the loop
        if ! currentPid=$(awk '/^PPid:/ { print $2 }' /proc/"$currentPid"/status 2>/dev/null); then
            break
        fi
    done

    # list all user-space processes (excluding kernel tasks) and warn the user for each
    # non-dep process running on any of the cores in configuredCores

    matchedPids=""

    # shellcheck disable=SC2010
    for pid in /proc/[0-9]*; do
        pid=${pid##*/}

        # skip if not all digits
        case "$pid" in
            ''|*[!0-9]*) continue ;;
        esac

        # check if /proc/$pid still there
        [ -d "/proc/$pid" ] || continue

        # skip kernel tasks, read all files in one shot
        #
        # NOTE:	on Linux, /proc/$pid/cmdline is a null-separated string of
        # command-line arguments for the given process.
        # POSIX shells do not support null bytes inside variables, so to avoid triggering errors like:
        #
        #   warning: command substitution: ignored null byte in input
        #
        # we read the cmdline file and replace null bytes with spaces.
        {
            cmdline=$( { tr '\0' ' ' < "/proc/$pid/cmdline"; } 2>/dev/null ) || continue
            [ -n "$cmdline" ] || continue

            procName=$(cat "/proc/$pid/comm") || continue
            procName=$(printf "%s" "$procName" | sed 's/ //g' | tr -d '\r\n')  # remove whitespaces and trim newline

            [ -r "/proc/$pid/stat" ] || continue
            cpuCore=$(awk '{print $39}' "/proc/$pid/stat" 2>/dev/null) || continue
        } || continue

        # skip processes that are not running on any of the configured cores
        if ! echo "$configuredCores" | grep -qw "$cpuCore"; then
            continue
        fi

        # skip DEP processes
        if is_dep_process "$procName"; then
            # this is a DEP process, skip it
            continue
        fi

        if [ -z "$matchedPids" ]; then
            matchedPids="name:$procName,PID:$pid,core:$cpuCore"
        else
            matchedPids="$matchedPids name:$procName,PID:$pid,core:$cpuCore"
        fi
    done

    if [ -n "$matchedPids" ]; then
        logwarn "the following processes/threads are running on one or multiple CPU cores which are also configured for DEP:"
        for p in $matchedPids; do
            # extract name, PID and core id from the formatted string (name:procName,PID:pid,core:cpuCore)
            procName=$(echo "$p" | cut -d',' -f1 | cut -d':' -f2)
            procPid=$(echo "$p" | cut -d',' -f2 | cut -d':' -f2)
            procCore=$(echo "$p" | cut -d',' -f3 | cut -d':' -f2)
            echo "${TAB}name: $procName, PID: $procPid, core: $procCore"
        done
        logwarn "Please ensure that these processes are not running on the same cores as DEP."
        echo "${TAB}DEP to work at its best requires good CPU isolation, which prevents its processing"
        echo "${TAB}threads from being interrupted from other unrelated userspace or kernelspace tasks."
        echo "${TAB}Consult the DEP manual for more information on how to achieve optimal CPU isolation."
        checkFailed=1
    fi

    if [ "$checkFailed" -eq 0 ]; then
        logok "DEP CPU configuration is valid - configured cores: $configuredCores"
    fi

    # check the scaling governor for each configured core
    for core in $configuredCores; do
        path="/sys/devices/system/cpu/cpu$core/cpufreq/scaling_governor"

        if [ -r "$path" ]; then
            governor=$(cat "$path" 2>/dev/null)
            if [ "$governor" = "performance" ]; then
                logok "CPU $core scaling governor set to performance"
            else
                logwarn "CPU $core scaling governor set to '$governor' - recommended to set to 'performance' if kernel supports it"
            fi
        else
            loginfo "CPU $core scaling governor not found or unreadable, skipping check"
        fi
    done
}

check_rng_speed() {
    if ! cmd_exists /usr/bin/time; then
        logwarn "'/usr/bin/time' not found, skipping /dev/random speed test"
        return
    fi

    if ! cmd_exists dd; then
        logwarn "'dd' not found, skipping /dev/random speed test"
        return
    fi

    # try to use more basic system utilities such as ps and sleep rather than timeout 
    dd if=/dev/random of=/dev/null bs=1024 count=1 iflag=fullblock >/tmp/dep_check_out 2>&1 &
    PID=$!
    sleep 1
    kill $PID > /dev/null 2> /dev/null
    wait $PID > /dev/null 2> /dev/null
    RETVAL=$?
    OUTPUT=$(cat /tmp/dep_check_out)
    if [ $RETVAL -ne 0 ]; then # dd did not complete successfully
        if [ -n "$OUTPUT" ]; then # some output was produced, dd probably had an error
            logerr "$OUTPUT"
        else # dd did not produce output
            logerr "dd was killed before producing any output. /dev/random is probably too slow"
        fi
        return
    fi # dd did not time out

    SPEED_TEST_OUTPUT=$({ /usr/bin/time -p dd if=/dev/random of=/dev/null bs=1024 count=1 iflag=fullblock; } 2>&1 | grep real)
    # At this stage, SPEED_TEST_OUTPUT will have a form like "real 0.00". Note that we use the GNU time here which only
    # contains two decimal point precision. This does not mean "0.00" took 0 seconds to finish but rather was less than 10 ms.
    # The following code converts this output to a millisecond value.
    
    # remove the "real" part of the output
    TIME=${SPEED_TEST_OUTPUT#real }
    SECS=${TIME%%.*} # grab the digits before the decimal point
    SUB_SECOND=${TIME#*.}
    SUB_SECOND=${SUB_SECOND%[0-9]*} # grab the first digit after the decimal point

    if [ "$SECS" -gt "0" ] || [ "$SUB_SECOND" -gt "0" ]; then
        logwarn "/dev/random is too slow (took $TIME seconds or more)"
    else
        logok "/dev/random is sufficiently fast (took less than 100 milliseconds)"
    fi

    # cleanup
    rm -rf /tmp/dep_check_out
}

load_config_files() {
    jsonSyntaxError=0
    if [ ! -e "$DANTE_JSON" ]; then
        logerr "$DANTE_JSON not found"
        fail
    fi

    if [ -L "$CONFIG_JSON" ]; then
        # check where the symlink points to
        target="$PATH_TO_BUNDLE_DIR/$(readlink "$PATH_TO_BUNDLE_DIR/config.json")"
        if [ -e "$target" ]; then
            target=$(realpath "$target")
        else
            logerr "$target does not exist at the symlink target pointed by '$PATH_TO_BUNDLE_DIR/config.json'"
            fail
        fi
    fi

    if [ ! -e "$CONFIG_JSON" ]; then
        logerr "$CONFIG_JSON not found"
        fail
    fi

    if ! "$JQ" empty "$DANTE_JSON" 2>jq_err; then
        logerr "syntax error detected in '$DANTE_JSON': $(cat jq_err)"
        jsonSyntaxError=1
    fi

    if ! "$JQ" empty "$CONFIG_JSON" 2>jq_err; then
        logerr "syntax error detected in '$CONFIG_JSON': $(cat jq_err)"
        jsonSyntaxError=1
    fi

    rm -f jq_err

    if [ "$jsonSyntaxError" -eq 1 ]; then
        fail
    fi

    # load once instead of parsing multiple times when needed
    logok "JSON configuration files loaded"
    DANTE_JSON_DATA=$(cat "$DANTE_JSON")
    CONFIG_JSON_DATA=$(cat "$CONFIG_JSON")
}

check_network_interfaces() {
    # first, check that all network interfaces specified in $DANTE_JSON actually exist
    interfaces=$(get_dante_json_array_elements ".network.interfaces")
    for iface in $interfaces; do
        if ! ip a show dev "$iface" >/dev/null 2>&1; then
            logerr "network interface $iface specified in $DANTE_JSON not found on the system"
        else
            logok "network interface $iface found"
        fi
    done

    # check that interfaceMode is set to a valid value, and if it is set to "Direct",
    # check network coalescing configuration
    if interfaceMode=$(get_dante_json_field ".network.interfaceMode"); then
        case $interfaceMode in
            Direct | Switched)
                loginfo "interfaceMode set to '$interfaceMode'"
                ;;
            *)
                logerr "invalid interfaceMode in dante.json: '$interfaceMode'. Possible values are: Direct, Switched"
                ;;
        esac
    else
        # default: Direct
        interfaceMode="Direct"
    fi

    # if interfaceMode is set to "Switched", checking network coalescing is deferred
    # to the check_clock_hardware_interfaces() function, which will check the interfaces
    # specified in the "hardwareInterfaces"
    if [ "$interfaceMode" = "Direct" ]; then
        for iface in $interfaces; do
            check_iface_coalesce "$iface"
        done
    else
        DEFER_COA_CHECK=1
    fi

    if ! json_field_exists "$DANTE_JSON_DATA" ".network.preferredLinkSpeed"; then
        return
    fi

    # check that preferredLinkSpeed is set to a valid value
    configuredSpeedValue=$(get_dante_json_field ".network.preferredLinkSpeed")

    case $configuredSpeedValue in
        LINK_SPEED_100M) ;;
        LINK_SPEED_1G) ;;
        LINK_SPEED_10G) ;;
        *)
            logwarn "preferredLinkSpeed '$configuredSpeedValue' in $DANTE_JSON is not supported."
            echo "${TAB}Possible values are: LINK_SPEED_100M, LINK_SPEED_1G, LINK_SPEED_10G"
            ;;
    esac
}

check_clock_hardware_interfaces() {
    hwTimestampingEnabled=0
    hwTsField=$(get_dante_json_field ".clock.enableHwTimestamping")
    hwTsDescription=""
    case "$hwTsField" in
        true)
            hwTimestampingEnabled=1
            hwTsDescription="hardware"
            ;;
        "v1")
            hwTimestampingEnabled=1
            hwTsDescription="PTPv1-only hardware"
            ;;
        false|"") hwTimestampingEnabled=0 ;;
        *) logerr "invalid enableHwTimestamping in dante.json: '$hwTsField'. Possible values are: true, false, \"v1\"" ;;
    esac

    dsaTagField=""
    if json_field_exists "$DANTE_JSON_DATA" ".clock.dsaTaggedPackets"; then
        if ! is_json_field_bool "$DANTE_JSON_DATA" ".clock.dsaTaggedPackets"; then
            logerr "'clock.dsaTaggedPackets' in dante.json is not a boolean value. Please set to either true or false"
            return
        fi

        dsaTagField=$(get_dante_json_field ".clock.dsaTaggedPackets")
        if [ "$dsaTagField" = true ]; then
            loginfo "DSA tagging in use"
        fi
    fi

    if [ "$hwTimestampingEnabled" -eq 0 ]; then
        loginfo "hardware timestamping is disabled in $DANTE_JSON"
        return
    fi

    logok "$hwTsDescription timestamping is enabled"

    # check that, if defined, hardwareInterfaces is an array and not empty
    hwIfacesCount=0
    hardwareInterfaces=""
    networkInterfacesCount=0

    if is_json_field_array "$DANTE_JSON_DATA" ".clock.hardwareInterfaces"; then
        hwIfacesCount=$(get_json_array_length "$DANTE_JSON_DATA" ".clock.hardwareInterfaces")
        if [ "$hwIfacesCount" -eq 0 ]; then
            logerr "'clock.hardwareInterfaces' in $DANTE_JSON is an empty array"
        else
            hardwareInterfaces=$(get_dante_json_array_elements ".clock.hardwareInterfaces")

            # make sure number of interfaces specified in network.interfaces and clock.hardwareInterfaces matches
            networkInterfacesCount=$(get_json_array_length "$DANTE_JSON_DATA" ".network.interfaces")
        fi
    elif json_field_exists "$DANTE_JSON_DATA" ".clock.hardwareInterfaces"; then
        logerr "'clock.hardwareInterfaces' in $DANTE_JSON is not an array"
    fi

    if [ "$dsaTagField" = true ] && [ "$networkInterfacesCount" -ne "$hwIfacesCount" ]; then
        logerr "number of interfaces in 'clock.hardwareInterfaces' ($hwIfacesCount) does not match number of interfaces in 'network.interfaces' ($networkInterfacesCount)"
        echo "${TAB}When using DSA, the virtual interfaces cannot directly make use of the IEEE1588 packet timestamping available"
        echo "${TAB}through the FEC. DEP supports using an alternative 'hardware' interface for timestamping when virtual interfaces"
        echo "${TAB}are used. Similar to the 'interfaces' field of dante.json, the 'hardwareInterfaces' field takes an array"
        echo "${TAB}of interface identifiers (names or indices). Each value in hardwareInterfaces must correspond to the same"
        echo "${TAB}positioned value in the 'interfaces' array."
    fi

    # from here, all checks are done only if hardwareInterfaces is populated
    if [ "$hwIfacesCount" -eq 0 ]; then
        return
    fi

    # check all interfaces specified in hardwareInterfaces actually exist
    previousInterface=""
    for iface in $hardwareInterfaces; do
        if [ -n "$previousInterface" ] && [ "$iface" = "$previousInterface" ]; then
            # already checked, skip it
            continue
        fi
        previousInterface="$iface"

        if ! ip a show dev "$iface" >/dev/null 2>&1; then
            logerr "network interface $iface specified in hardwareInterfaces in $DANTE_JSON not found on the system"
        else
            logok "network interface $iface found"
            # this is a result of the check_network_interfaces() function letting us know that
            # the interfaceMode is set to "Switched" and the 'real' network interfaces on which network
            # coalescing should be checked are those in hardwareInterfaces, not in interfaces
            if [ "$DEFER_COA_CHECK" -eq 1 ]; then
                check_iface_coalesce "$iface"
            fi
        fi
    done
}

# This function checks whether a device node is configured correctly
# in an OCI-compliant containter runtime configuration file (e.g., config.json).
#
# It relies on cgroupVersion being set in dante.json for its functionality: this is
# because different cgroup versions require different ways to configure device nodes
# in the configuration file.
is_device_node_configured() {
    deviceNode="$1"

    fileMode=384 # 0600 in octal
    gid=0
    uid=0
    allow="true"
    access="rw"

    # shellcheck disable=SC2012
    deviceType=$(ls -l "$deviceNode" | cut -c1)
    if [ "$deviceType" = "c" ] || [ "$deviceType" = "b" ]; then
        :
    else
        logerr "parsing device node '$deviceNode' type failed, it's not a character or block device"
    fi

    # shellcheck disable=SC2012
    major=$(ls -l "$deviceNode" | awk '{print $5}' | sed 's/,//')
    # shellcheck disable=SC2012
    minor=$(ls -l "$deviceNode" | awk '{print $6}')

    if ! json_field_exists "$DANTE_JSON_DATA" ".platform.cgroupVersion"; then
        # assume cgroup v1
        logwarn "assuming cgroup v1 - please set 'cgroupVersion' in dante.json to allow this check to work correctly"
        cgroupVersion="1"
    else
        cgroupVersion=$(get_dante_json_field ".platform.cgroupVersion")
    fi

    # for cgroup v1, device node needs to be also specified in linux.resources.devices[]
    foundInLinuxResources=0
    if [ "$cgroupVersion" = "1" ]; then
        # entries in linux.resources.devices[] are of the form:
        # {
        #   "allow": true,
        #   "type": "c",
        #   "major": 189,
        #   "minor": 1,
        #   "access": "rw"
        # }
        # so it's not easy to identify whether a device node is configured or not.
        # What we do is check whether an entry with the same allow, type, major, minor and access fields exists,
        # and if it doesn't, print out what it should look like.

        linuxResourcesDevices=$(get_config_json_array_elements ".linux.resources.devices")
        for entry in $linuxResourcesDevices; do
            if [ "$(get_json_field "$entry" ".allow")" = "$allow" ] &&
                [ "$(get_json_field "$entry" ".type")" = "$deviceType" ] &&
                [ "$(get_json_field "$entry" ".major")" = "$major" ] &&
                [ "$(get_json_field "$entry" ".minor")" = "$minor" ] &&
                [ "$(get_json_field "$entry" ".access")" = "$access" ]; then
                foundInLinuxResources=1
            fi
        done

        if [ "$foundInLinuxResources" -eq 0 ]; then
            # shellcheck disable=SC2089
            expectedEntry="{\"allow\": $allow,\"type\": \"$deviceType\",\"major\": $major,\"minor\": $minor,\"access\": \"$access\"}"
            logerr "device node '$deviceNode' not found in linux.resources.devices[], check that an"
            echo "${TAB}identical entry like the following one exists in $CONFIG_JSON:"
            # shellcheck disable=SC2090
            # shellcheck disable=SC2086
            echo $expectedEntry | $JQ | sed 's/^/            /'
        fi
    else
        foundInLinuxResources=1
    fi

    foundInLinuxDevices=0
    misconfiguredEntryFound=0
    linuxDevices=$(get_config_json_array_elements ".linux.devices")
    linuxDeviceEntryFields=".type:$deviceType .major:$major .minor:$minor .fileMode:$fileMode .gid:$gid .uid:$uid"

    for entry in $linuxDevices; do
        if [ "$(get_json_field "$entry" ".path")" = "$deviceNode" ]; then
            foundInLinuxDevices=1

            for f in $linuxDeviceEntryFields; do
                field=$(echo "$f" | cut -d':' -f1)
                expectedValue=$(echo "$f" | cut -d':' -f2)

                # check if the entry has the correct field and value
                configuredValue=$(get_json_field "$entry" "$field")

                if [ "$configuredValue" != "$expectedValue" ]; then
                    logerr "device node '$deviceNode' has incorrect $field in $CONFIG_JSON: expected '$expectedValue', got '$configuredValue' in linux.devices[]"
                    misconfiguredEntryFound=1
                fi
            done
        fi
    done

    if [ $foundInLinuxDevices -eq 0 ]; then
        logerr "device node '$deviceNode' not found in $CONFIG_JSON: expected entry with path '$deviceNode' in linux.devices[]"
        echo "${TAB}Please add the device node to the 'linux.devices[]' array in $CONFIG_JSON."
        return 0
    fi

    if [ $foundInLinuxDevices -eq 1 ] && [ $foundInLinuxResources -eq 1 ] && [ $misconfiguredEntryFound -eq 0 ]; then
        logok "'$deviceNode' configured correctly"
        return 1
    fi
}

# For both the primary and secondary (if being used) network interfaces, perform a
# preliminary check of all timestamping modes using the PTP test tool. For each mode,
# the tool will attempt to create a network socket confgured with that mode. This is
# therefore a much more definitive check than relying solely on capability reports
# provided by ethtool (which may not be 100% accurate).
#
# If DSA tagging is set and software timestamping has been configured in dante.json,
# the preliminary checks for hardware timestamping are simply skipped as the hardware
# interface(s) are deliberately ignored (even if defined).
#
# The return value is an array of strings, where each string is itself a collection
# of the following substrings separated by a '@' character:
#
# - Timestamping interface
# - The timestamping mode description (hardware/software/PTPv1-only hardware)
# - A 1 or 0 indicating if this mode is the one in dante.json
# - The return value from the PTP test tool (0 = success, 1 = failure)
# - The complete command line needed to run either a full timestamping test (if the
#   check succeeded) or to run the preliminary check again (if it failed)
#
# NOTE: to allow the caller to iterate through the array using for(), the substrings
# cannot have any spaces at all. For this reason, a string will have all its spaces
# substituted with '+' characters. The caller, when looking at an array entry, is
# expected to replace the '+'s with spaces again and to simply use cut -d'@' to get
# at each substring.
timestamping_preliminary_checks() {
    interfaceName=$1
    danteJsonEnableHwTs=$2
    dsaTaggedPackets=$3
    hwInterfaceName=$4

    # Create an array of modes to test, where the mode configured in dante.json is the
    # first entry. This is done so that the caller, when reporting the check results,
    # will show that mode first.
    if [ "$danteJsonEnableHwTs" = true ]; then
        configuredTsMode="-hw"
        tsModesToTest="-hw -hwv1 -sw"
    elif [ "$danteJsonEnableHwTs" = "v1" ]; then
        configuredTsMode="-hwv1"
        tsModesToTest="-hwv1 -hw -sw"
    else
        configuredTsMode="-sw"
        if [ "$dsaTaggedPackets" = true ]; then
            tsModesToTest="-sw"
        else
            tsModesToTest="-sw -hw -hwv1"
        fi
    fi

    for tsMode in $tsModesToTest; do
        if [ "$tsMode" = "-hw" ]; then
            tsModeDescription="hardware"
        elif [ "$tsMode" = "-hwv1" ]; then
            tsModeDescription="PTPv1-only hardware"
        else
            tsModeDescription="software"
        fi

        timestampingInterface=$interfaceName
        dsaArgs=""

        # Any DSA arguments will be appended to the end of the standard PTP test tool command line
        # arguments. We don't want any trailing spaces for the command lines we show the user, and
        # so the DSA arguments will have a leading space.
        if [ "$dsaTaggedPackets" = true ]; then
            if [ "$tsMode" = "-hw" ] || [ "$tsMode" = "-hwv1" ]; then
                dsaArgs=" -dsa -hwi $hwInterfaceName"
                timestampingInterface=$hwInterfaceName
            else
                dsaArgs=" -dsa"
            fi
        fi

        ptpTestToolCmdLine="${PTP_TEST} -i $interfaceName $tsMode$dsaArgs" # No space between $tsMode and $dsaArgs

        # shellcheck disable=SC2086
        ${ptpTestToolCmdLine} -c > /dev/null 2>&1
        retval=$?
        if [ "$tsMode" = "$configuredTsMode" ]; then
            isConfiguredTsMode=1
        else
            isConfiguredTsMode=0
        fi

        # If the check failed, the command line we pass back will be to run a preliminary
        # rather than a full check.
        if [ $retval -eq 1 ]; then
            ptpTestToolCmdLine="$ptpTestToolCmdLine -c"
        fi

        # Substitute all spaces in the output string with the '+' character
        echo "$timestampingInterface@$tsModeDescription@$isConfiguredTsMode@$retval@$ptpTestToolCmdLine" | sed 's/ /+/g'
    done
}

check_timestamping_config() {
    hwTsField=$(get_dante_json_field ".clock.enableHwTimestamping")
    networkInterfaces=$(get_dante_json_array_elements ".network.interfaces")

    if json_field_exists "$DANTE_JSON_DATA" ".clock.dsaTaggedPackets"; then
        if ! is_json_field_bool "$DANTE_JSON_DATA" ".clock.dsaTaggedPackets"; then
            logwarn "'clock.dsaTaggedPackets' is invalid - skipping timestamping configuration check"
            return
        fi
        dsaTaggedPackets=$(get_dante_json_field ".clock.dsaTaggedPackets")
    else
        dsaTaggedPackets=false
    fi

    clockHwInterfaces=""
    if [ "$hwTsField" = true ] || [ "$hwTsField" = "v1" ]; then
        # Check if AES67 is being supported (if so, PTPv1 hardware timestamping cannot be used)
        aes67Supported=$(get_dante_json_field ".audio.aes67Supported")

        if [ "$aes67Supported" = true ] && [ "$hwTsField" = "v1" ]; then
            logerr "AES67 cannot be supported with PTPv1 timestamping."
            echo "${TAB}To enable AES67, enableHwTimestamping must be set to either true or false."
            return
        fi

        # Hardware timestamping is in use, so check PHCs.
        #
        # If dsaTaggedPackets is false, the NICs whose PHCs need to be checked are those in
        # .network.interfaces.
        if [ "$dsaTaggedPackets" = true ]; then
            clockHwInterfaces=$(get_dante_json_array_elements ".clock.hardwareInterfaces")

            # $clockHwInterfaces will be empty if it is either undefined or invalid.
            # Error and return in this case.
            if [ -z "$clockHwInterfaces" ]; then
                logerr "'clock.hardwareInterfaces' is undefined or invalid - unable to check PHC(s) or run preliminary hardware timestamping checks"
                return
            fi

            ifacesToCheck=$clockHwInterfaces
        else
            ifacesToCheck=$networkInterfaces
        fi

        previousPHC=""
        for iface in $ifacesToCheck; do
            # $iface needs to be associated with a PHC device for hardware timestamping to work
            phcName=""

            ptpPath="/sys/class/net/$iface/device/ptp"
            if [ -d "$ptpPath" ]; then
                for d in "$ptpPath"/*; do
                    if [ -d "$d" ] && [ -f "$d/clock_name" ]; then
                        phcName=$(basename "$d")
                        deviceNode="/dev/$phcName"
                    fi
                done
            elif cmd_exists ethtool; then
                phc=$(ethtool -T "$iface" 2>/dev/null | awk "/PTP Hardware Clock:/ {print \$4}")
                if [ -n "$phc" ] && [ "$phc" != "none" ]; then
                    phcName="ptp$phc"
                    deviceNode="/dev/$phcName"
                fi
            fi

            if [ -z "$phcName" ]; then
                logerr "no PTP device found for $iface."
                echo "${TAB}hardware timestamping cannot be enabled when using this interface."
                return
            fi

            if [ -n "$previousPHC" ] && [ "$previousPHC" = "$phcName" ]; then
                # already checked this node, skip it
                continue
            fi
            previousPHC="$phcName"

            loginfo "$iface: PHC device found ($phcName)"

            if [ ! -e "$deviceNode" ]; then
                logerr "corresponding PTP device node '$deviceNode' not found for $iface interface"
                continue
            fi

            # we have a PHC node, has the user configured it in config.json?
            is_device_node_configured "$deviceNode"
        done
    fi

    # Perform preliminary timestamping checks using the PTP timestamping test tool
    if [ "$PTP_TEST_COMPATIBLE" -ne 1 ]; then
        # Do a test run of the tool to see if the host's glibc really is incompatible.
        # If it is, the checks are skipped.
        ${PTP_TEST} -h > /dev/null 2>&1
        if [ $? -ne 0 ]; then
            logwarn "host-provided glibc ($installedGlibc) is older than required ($REQUIRED_GLIBC_PTP_TEST) for ${PTP_TEST}"
            echo "${TAB}Skipping preliminary timestamping checks"
            return
        fi
    fi

    # Initially, we assume that all timestamping modes will pass their preliminary checks.
    # If EITHER the primary or secondary interface has a failed or skipped check for a
    # particular timestamping mode, that mode has its check marked as having failed.
    hwTsPrelimCheckOk=1
    hwV1TsPrelimCheckOk=1
    swTsPrelimCheckOk=1

    if [ "$dsaTaggedPackets" = true ] && [ "$hwTsField" = false ]; then
        loginfo "DSA tagging in use along with software timestamping - skipping preliminary hardware timestamping checks."
        echo "${TAB}To perform these checks, please rerun this script after turning on hardware timestamping and ensuring"
        echo "${TAB}that 'clock.hardwareInterfaces' contains valid entries."
        hwTsPrelimCheckOk=0
        hwV1TsPrelimCheckOk=0
    fi

    primaryCheckCmdsAndResults=""
    secondaryCheckCmdsAndResults=""

    ifaceIndex=0
    hwIface=""
    for iface in $networkInterfaces; do
        if [ -n "$clockHwInterfaces" ]; then
            hwIface=$(echo "$DANTE_JSON_DATA" | "$JQ" -cr ".clock.hardwareInterfaces[$ifaceIndex]")
            if [ "$hwIface" = null ]; then
                # We'll hit this if clock.hardwareInterfaces has fewer entries than
                # network.interfaces. This error will already have been reported in
                # an earlier check, but it will also interfere with proper timestamping
                # checking and so log another error here and exit this function.
                logerr "Unable to perform preliminary timestamping check for interface $iface, as associated 'clock.hardwareInterfaces' entry is missing"
                return
            fi
        fi

        # NOTE: if we're now checking the secondary interface, it's possible that
        # $hwIface is common to both the primary and secondary. The preliminary checks
        # below will end up checking hardware timestamping on $hwIface a second time -
        # even though this is arguably redundant, we still want to do this in order to
        # be able to show the user the full timestamping test command lines (which will
        # feature the primary and secondary interfaces respectively).
        checkCmdsAndResults=$(timestamping_preliminary_checks "$iface" "$hwTsField" "$dsaTaggedPackets" "$hwIface")

        if [ "$ifaceIndex" -eq 0 ]; then
            primaryCheckCmdsAndResults="$checkCmdsAndResults"
        else
            secondaryCheckCmdsAndResults="$checkCmdsAndResults"
        fi

        ifaceIndex=$((ifaceIndex+1))
    done

    # Keep track of the PTP test tool commands that can be used to do full tests for all
    # interfaces and each timestamping mode. Depending on which modes is later deemed as
    # recommended, the appropriate set of commands will be shown to the user.
    hwTsFullCheckCmds=""
    hwV1TsFullCheckCmds=""
    swTsFullCheckCmds=""

    # We need to report on the success/failure of the check of the timestamping mode in
    # dante.json
    danteJsonHwTsDescription=""
    danteJsonHwTsFullCheckCmds=""
    danteJsonFailedPrelimCheckCmds=""

    # Display all preliminary check results first
    for check in $primaryCheckCmdsAndResults $secondaryCheckCmdsAndResults; do
        iface=$(echo "$check" | cut -d'@' -f1)
        hwTsDescription=$(echo "$check" | cut -d'@' -f2 | sed 's/+/ /g') # Restore any spaces
        isDanteJsonSetting=$(echo "$check" | cut -d'@' -f3)
        checkRetval=$(echo "$check" | cut -d'@' -f4)

        # Don't restore spaces in the command line for now, as it will be added to another
        # array. Spaces will only be restored if the command line ends up being displayed.
        ptpTestToolCmdLine=$(echo "$check" | cut -d'@' -f5)

        logMsgPrefix="Preliminary check of $hwTsDescription timestamping on interface $iface"

        if [ "$checkRetval" -eq 0 ]; then
            if [ "$isDanteJsonSetting" -eq 1 ]; then
                logok "$logMsgPrefix passed"

                danteJsonHwTsDescription="$hwTsDescription"
                danteJsonHwTsFullCheckCmds="$danteJsonHwTsFullCheckCmds $ptpTestToolCmdLine"
            else
                loginfo "$logMsgPrefix passed"
            fi

            # Add the command line to the ones for the current timestamping mode
            case "$hwTsDescription" in
                "hardware")
                    hwTsFullCheckCmds="$hwTsFullCheckCmds $ptpTestToolCmdLine"
                    ;;
                "software")
                    swTsFullCheckCmds="$swTsFullCheckCmds $ptpTestToolCmdLine"
                    ;;
                *)
                    # PTPv1 hardware
                    hwV1TsFullCheckCmds="$hwV1TsFullCheckCmds $ptpTestToolCmdLine"
                    ;;
            esac
        else
            if [ "$isDanteJsonSetting" -eq 1 ]; then
                logerr "$logMsgPrefix failed"

                danteJsonHwTsDescription="$hwTsDescription"
                danteJsonFailedPrelimCheckCmds="$danteJsonFailedPrelimCheckCmds $ptpTestToolCmdLine"
            else
                logwarn "$logMsgPrefix failed"
            fi

            case "$hwTsDescription" in
                "hardware")
                    hwTsPrelimCheckOk=0
                    ;;
                "software")
                    swTsPrelimCheckOk=0
                    ;;
                *)
                    # PTPv1 hardware
                    hwV1TsPrelimCheckOk=0
                    ;;
            esac
        fi
    done

    # If no timestamping modes passed their preliminary check, log an error and return
    if [ "$hwTsPrelimCheckOk" -eq 0 ] && [ "$hwV1TsPrelimCheckOk" -eq 0 ] && [ "$swTsPrelimCheckOk" -eq 0 ]; then
        if [ -z "$secondaryCheckCmdsAndResults" ]; then # No secondary
            logerr "Primary interface does not support any of the required timestamping modes and cannot be used."
            echo "${TAB}Please specify an alternative interface in dante.json."
        else
            logerr "Primary and secondary interfaces do not support any of the required timestamping modes in common."
            echo "${TAB}Please remove the secondary interface from dante.json and/or change the interfaces."
        fi
        return
    fi

    # Internal function for displaying an array of PTP test tool commands. The array will
    # have two entries if using a primary and secondary interface, one otherwise. These
    # commands, obtained from timestamping_preliminary_checks(), need to have their
    # placeholder '+' characters changed back to spaces.
    display_ptp_test_tool_cmds() {
        for cmd in $1; do
            restoredCmd=$(echo "$cmd" | sed 's/+/ /g')
            echo "${TAB}$restoredCmd"
        done
    }

    display_full_timestamping_test_advice() {
        hwTsDescription=$1
        hwTsFullCheckCmds=$2
        referToReadme=$3

        echo "${TAB}Please be sure to run the following command(s) to perform a full $hwTsDescription timestamping test:"
        display_ptp_test_tool_cmds "$hwTsFullCheckCmds"
        if [ "$referToReadme" -eq 1 ]; then
            echo "${TAB}Please see $PTP_TEST_README for advice on running a full test."
        fi
    }

    # Determine the recommended timestamping setting
    recommendedHwTsDescription=""
    recommendedHwTsFullCheckCmds=""

    if [ "$hwTsPrelimCheckOk" -eq 1 ]; then
        recommendedHwTsDescription="hardware"
        recommendedHwTsFullCheckCmds="$hwTsFullCheckCmds"
    elif [ "$swTsPrelimCheckOk" -eq 1 ]; then
        if [ "$hwV1TsPrelimCheckOk" -eq 1 ]; then
            recommendedHwTsDescription="either software or PTPv1-only hardware"
            recommendedHwTsFullCheckCmds="$swTsFullCheckCmds $hwV1TsFullCheckCmds"
        else
            recommendedHwTsDescription="software"
            recommendedHwTsFullCheckCmds="$swTsFullCheckCmds"
        fi
    else
        recommendedHwTsDescription="PTPv1-only hardware"
        recommendedHwTsFullCheckCmds="$hwV1TsFullCheckCmds"
    fi

    referToReadme=1

    # Check whether the timestamping setting in dante.json passed or not
    if [ -z "$danteJsonFailedPrelimCheckCmds" ]; then
        logok "All preliminary checks passed for $danteJsonHwTsDescription timestamping."
        if [ "$recommendedHwTsDescription" = "$danteJsonHwTsDescription" ]; then
            echo "${TAB}Based on the checks above, $danteJsonHwTsDescription timestamping is the recommended setting."
        fi
        display_full_timestamping_test_advice "$danteJsonHwTsDescription" "$danteJsonHwTsFullCheckCmds" "$referToReadme"

        # If the dante.json setting is the recommended one, there's nothing more to do
        if [ "$recommendedHwTsDescription" = "$danteJsonHwTsDescription" ]; then
            return
        fi

        # Prepare to offer advice on an alternative, recommended setting
        however=" however,"
        referToReadme=0 # We've displayed it already, so don't do it again
    else
        logwarn "Preliminary check(s) failed for $danteJsonHwTsDescription timestamping."
        echo "${TAB}Therefore, $danteJsonHwTsDescription timestamping cannot be used with the interface(s) configured in dante.json."
        echo "${TAB}For more details on the failure(s), the following command(s) can be run:"
        display_ptp_test_tool_cmds "$danteJsonFailedPrelimCheckCmds"

        # Suggest the recommended setting(s)
        however=""
    fi

    loginfo "Based on the checks above,$however the recommended setting is $recommendedHwTsDescription timestamping."
    if [ "$recommendedHwTsDescription" = "hardware" ]; then
        if [ "$danteJsonHwTsDescription" = "PTPv1-only hardware" ]; then
            echo "${TAB}This will enable use of PTPv2 features such as AES67 and site/domain unicast clocking."
        else
            echo "${TAB}This will provide better clocking stability."
        fi
    elif [ "$recommendedHwTsDescription" = "either software or PTPv1-only hardware" ]; then
        # Explain the tradeoffs for each mode, however only display the full test advice
        # if it has not already been displayed.
        echo "${TAB}With software, clocking is not as accurate as hardware but PTPv2 features such as"
        echo "${TAB}AES67 and site/domain unicast clocking can be used."
        if [ "$danteJsonHwTsDescription" != "software" ]; then
            display_full_timestamping_test_advice "software" "$swTsFullCheckCmds" "$referToReadme"
            referToReadme=0
        fi
        echo "${TAB}With PTPv1-only hardware, clocking will be more stable but PTPv2 features such as"
        echo "${TAB}AES67 and site/domain unicast clocking cannot be used."
        if [ "$danteJsonHwTsDescription" != "PTPv1-only hardware" ]; then
            display_full_timestamping_test_advice "PTPv1-only hardware" "$hwV1TsFullCheckCmds" "$referToReadme"
        fi

        # Advice displayed, so nothing more to do
        return
    elif [ "$recommendedHwTsDescription" = "software" ]; then
        # If v1 was configured but is not supported, explain that PTPv2 can now be used
        if [ -n "$danteJsonFailedPrelimCheckCmds" ] && [ "$danteJsonHwTsDescription" = "PTPv1-only hardware" ]; then
            echo "${TAB}Clocking is not as accurate as hardware, but PTPv2 features such as AES67 and"
            echo "${TAB}site/domain unicast clocking can be used."
        fi
    else # $recommendedHwTs = "v1"
        # If software was configured but is not supported, explain that PTPv2 can't be used
        if [ -n "$danteJsonFailedPrelimCheckCmds" ] && [ "$danteJsonHwTsDescription" = "software" ]; then
            echo "${TAB}This will provide better clocking stability, but PTPv2 features such as AES67 and"
            echo "${TAB}site/domain unicast clocking cannot be used."
        fi
    fi

    display_full_timestamping_test_advice "$recommendedHwTsDescription" "$recommendedHwTsFullCheckCmds" "$referToReadme"
}

check_hw_clock_config() {
    if ! json_field_exists "$DANTE_JSON_DATA" ".hardwareClock.useHwClock"; then
        return
    fi

    if ! is_json_field_bool "$DANTE_JSON_DATA" ".hardwareClock.useHwClock"; then
        logerr "'hardwareClock.useHwClock' in dante.json is not a boolean value. Please set to either true or false"
        return
    fi

    useHwClockField=$(get_dante_json_field ".hardwareClock.useHwClock")

    if [ "$useHwClockField" = false ]; then
        return
    fi

    loginfo "hardware clock enabled in $DANTE_JSON"

    circuitNameField=$(get_dante_json_field ".hardwareClock.circuitName")
    if [ -z "$circuitNameField" ]; then
        logerr "'hardwareClock.circuitName' is not set in $DANTE_JSON."
        echo "${TAB}Please specify the circuit name for the hardware clock."
    fi

    i2cBusField=$(get_dante_json_field ".hardwareClock.i2cBus")
    if [ "$i2cBusField" = null ]; then
        i2cBusField="/dev/i2c-0" # default value
    fi

    if [ ! -e "$i2cBusField" ]; then
        logerr "i2c bus device node '$i2cBusField' not found on the system."
        echo "${TAB}Please check the 'hardwareClock.i2cBus' field in $DANTE_JSON."
    else
        is_device_node_configured "$i2cBusField"
    fi

    extClockInputDevField=$(get_dante_json_field ".hardwareClock.extClockInputDev")
    if [ "$extClockInputDevField" = null ]; then
        extClockInputDevField="/dev/extclkin" # default value
    fi

    if [ ! -e "$extClockInputDevField" ]; then
        logerr "external clock input device node '$extClockInputDevField' not found on the system."
        echo "${TAB}Please check the 'hardwareClock.extClockInputDev' field in $DANTE_JSON."
    else
        is_device_node_configured "$extClockInputDevField"
    fi
}

# Check if an encoding value is valid.
# Returns:
#   - 1 if the encoding is a standard PCM encoding (PCM16, PCM24, PCM32)
#   - 2 if the encoding is a custom value (between 0x1300 and 0x13FF inclusive)
#   - 0 otherwise (invalid encoding)
is_encoding_valid() {
    encoding="$1"

    encodingNumeric=$(printf "%d" "$encoding" 2>/dev/null)
    # shellcheck disable=SC2181
    if [ $? -eq 0 ]; then
        # check if encoding is in 0x1300–0x13FF range
        case "$encoding" in
            0x13[0-9A-Fa-f][0-9A-Fa-f])
                if [ "$encodingNumeric" -ge 4864 ] && [ "$encodingNumeric" -le 5119 ]; then
                    echo 2
                    return
                fi
                ;;
        esac
    fi

    # standard PCM encodings
    case "$encoding" in
        PCM16|PCM24|PCM32)
            echo 1
            ;;
        *)
            echo 0
            ;;
    esac
}

check_encodings()
{
    defaultEncoding=""

    if json_field_exists "$DANTE_JSON_DATA" "audio.defaultEncoding"; then
        defaultEncoding=$(get_dante_json_field ".audio.defaultEncoding")
        encodingType=$(is_encoding_valid "$defaultEncoding")
        if [ "$encodingType" -eq 0 ]; then
            logerr "defaultEncoding $defaultEncoding is invalid in $DANTE_JSON"
        elif [ "$encodingType" -eq 2 ]; then
            defaultEncoding=$(echo "$defaultEncoding" | tr '[a-f]' '[A-F]')
        fi
        logwarn "the 'defaultEncoding' field should no longer be used and will be removed in future versions of DEP."
        echo "${TAB}The default should instead be specified as the first entry in 'supportedEncodings'"
    fi

    if [ -z "$defaultEncoding" ] && ! json_field_exists "$DANTE_JSON_DATA" ".audio.supportedEncodings"; then
        loginfo "neither 'supportedEncodings' nor 'defaultEncoding' specified - PCM24 will be used by default"
        return
    fi

    if ! is_json_field_array "$DANTE_JSON_DATA" ".audio.supportedEncodings"; then
        logerr "'supportedEncodings' in $DANTE_JSON is not an array"
        return
    fi

    supportedEncodingsCount=$(get_json_array_length "$DANTE_JSON_DATA" ".audio.supportedEncodings")
    if [ "$supportedEncodingsCount" -eq 0 ]; then
        logerr "'supportedEncodings' cannot be specified as empty in $DANTE_JSON"
        return
    fi

    supportedEncodings=$(get_dante_json_array_elements ".audio.supportedEncodings")

    # check for duplicates in supportedEncodings
    if echo "$DANTE_JSON_DATA" | "$JQ" -e '.audio.supportedEncodings | sort | group_by(.) | any(length > 1)' > /dev/null; then
        logerr "'supportedEncodings' contains duplicate entries in $DANTE_JSON"
    fi

    customEncodingCount=0
    loopIndex=0

    for encoding in $supportedEncodings; do
        loopIndex=$((loopIndex+1))
        encodingType=$(is_encoding_valid "$encoding")

        if [ "$encodingType" -eq 0 ]; then
            logerr "'supportedEncodings' contains the invalid encoding $encoding in $DANTE_JSON"
        elif [ "$encodingType" -eq 2 ]; then
            customEncodingCount=$((customEncodingCount+1))
            if [ $customEncodingCount -gt 4 ]; then
                logerr "'supportedEncodings' cannot contain more than 4 custom encodings in $DANTE_JSON"
            fi
            # convert encoding string to uppercase
            encoding=$(echo "$encoding" | tr '[a-f]' '[A-F]')
        fi

        # if defaultEncoding is specified and we are looping over supportedEncodings,
        # check that it matches the first entry in supportedEncodings
        if [ -n "$defaultEncoding" ] && [ "$loopIndex" -eq 1 ]; then
            if [ "$encoding" != "$defaultEncoding" ]; then
                logerr "'supportedEncodings' does not match the specified 'defaultEncoding' in $DANTE_JSON"
            fi
        fi
    done
}

check_old_files() {
    mainPath=${DEP_PATH}"/"

    oldRootfsFound=0
    if [ -f "$mainPath/dante_data/images/1/rootfs_squash" ]; then
        oldRootfsFound=1
    fi

    # any old file around?
    if ls -1 "$mainPath" | grep -qE '^(runc|fixer|dep_util.sh|dep.service|select_image.sh)$' || [ $oldRootfsFound -eq 1 ]; then
        loginfo "These files can be removed to reduce disk usage:"
    else
        return
    fi

    if [ -f "$mainPath/runc" ]; then
        echo "${TAB}runc (old container runtime)"
    fi
    if [ -f "$mainPath/fixer" ]; then
        echo "${TAB}fixer (deprecated and no longer used)"
    fi
    if [ -f "$mainPath/dep_util.sh" ]; then
        echo "${TAB}dep_util.sh (deprecated and no longer used)"
    fi
    if [ -f "$mainPath/dep.service" ]; then
        echo "${TAB}dep.service (old systemd service file, a new updated version is available in the development subdirectory)"
    fi
    if [ -f "$mainPath/select_image.sh" ]; then
        echo "${TAB}select_image.sh (not used anymore)"
    fi
    if [ "$oldRootfsFound" -eq 1 ]; then
        echo "${TAB}dante_data/images/1/rootfs_squash (second rootfs image deprecated and no longer used)"
    fi
}

### main logic

# check we are running as root
check_uid

# make sure the first arg is a valid path
check_arg "$1"

check_runtime_deps

load_config_files

check_architecture
check_kernel_config
check_cgroups
check_cpu_config
check_rng_speed
check_network_interfaces
check_clock_hardware_interfaces
check_timestamping_config
check_hw_clock_config
check_encodings
check_old_files

#
# Copyright © 2022-2025 Audinate Pty Ltd ACN 120 828 006 (Audinate). All rights reserved.
#
