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

1803 lines
70 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# 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 0x13000x13FF 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.
#