op-packages/qosmate/etc/init.d/qosmate
github-actions[bot] 297bc561d7 🔥 Sync 2026-03-08 08:46:06
2026-03-08 08:46:06 +08:00

2574 lines
90 KiB
Bash

#!/bin/sh /etc/rc.common
# shellcheck disable=SC3043,SC1091,SC2155,SC3020,SC3010,SC2016,SC3060,SC3003,SC3015,SC3044
START=99
STOP=99
USE_PROCD=1
VERSION="dev"
UPD_CHANNEL="release"
EXTRA_COMMANDS="check_version update auto_setup auto_setup_noninteractive validate_custom_rules health_check"
EXTRA_HELP="
check_version Check for updates
update Update qosmate
auto_setup Automatically configure qosmate
auto_setup_noninteractive Automatically configure qosmate with no interaction
validate_custom_rules Validate custom rules
health_check Check if QoSmate is properly configured and running"
REQUIRED_PACKAGES="kmod-sched ip-full kmod-veth tc-full kmod-netem kmod-sched-ctinfo kmod-ifb kmod-sched-cake kmod-sched-red luci-lib-jsonc lua jq coreutils-sleep"
### Utility vars
_NL_='
'
DEFAULT_IFS=" ${_NL_}"
IFS="$DEFAULT_IFS"
### repo-related vars
# GH repo author (can be overriden for testing)
: "${QOSMATE_REPO_AUTHOR:=hudra0}"
MAIN_BRANCH_BACKEND=main
MAIN_BRANCH_FRONTEND=main
# GH API URLs (can be overridden)
: "${QOSMATE_GH_API_URL:="https://api.github.com/repos/${QOSMATE_REPO_AUTHOR}"}"
: "${QOSMATE_GH_API_URL_BACKEND:="${QOSMATE_GH_API_URL}/qosmate"}"
: "${QOSMATE_GH_API_URL_FRONTEND:="${QOSMATE_GH_API_URL}/luci-app-qosmate"}"
# GH raw backend URL (can be overridden)
: "${QOSMATE_GH_RAW_URL_BACKEND_MAIN:="https://raw.githubusercontent.com/${QOSMATE_REPO_AUTHOR}/qosmate/${MAIN_BRANCH_BACKEND}"}"
### Local paths
QOSMATE_D=/etc/qosmate.d
QOSMATE_CFG_FILE=/etc/config/qosmate
TMP_CFG_DIR=/tmp/qosmate_tmp
TMP_CFG_FILE=$TMP_CFG_DIR/qosmate
QOSMATE_DEFAULTS_FILE=${QOSMATE_D}/qosmate-defaults
QOSMATE_CUSTOM_RULES_FILE=${QOSMATE_D}/custom_rules.nft
QOSMATE_INLINE_RULES_FILE=${QOSMATE_D}/inline_dscptag.nft
QOSMATE_RUN_DIR=/tmp/qosmate
# Local path of JS files
QOSMATE_UPD_DIR=/var/run/qosmate-update
### Components
QOSMATE_COMPONENTS="BACKEND FRONTEND"
QOSMATE_FILES_REG_PATH_BACKEND=${QOSMATE_D}/backend_reg.md5
QOSMATE_FILES_REG_PATH_FRONTEND=${QOSMATE_D}/frontend_reg.md5
### Backend files
QOSMATE_SERVICE_PATH=/etc/init.d/qosmate
QOSMATE_MAIN_SCRIPT=/etc/qosmate.sh
QOSMATE_HOTPLUG_SCRIPT=/etc/hotplug.d/iface/13-qosmateHotplug
# stores the version and update channel
QOSMATE_VERSION_FILE_BACKEND=$QOSMATE_SERVICE_PATH
QOSMATE_FILE_TYPES_BACKEND="GEN EXTRA" # change the value if adding more types
QOSMATE_AUTORATE_SCRIPT=/etc/qosmate-autorate.sh
QOSMATE_AUTORATE_TC_SCRIPT=/etc/qosmate-autorate-tc.sh
QOSMATE_AUTORATE_SERVICE=/etc/init.d/qosmate-autorate
QOSMATE_GEN_FILES_BACKEND="
$QOSMATE_SERVICE_PATH
$QOSMATE_MAIN_SCRIPT
$QOSMATE_DEFAULTS_FILE
$QOSMATE_AUTORATE_SCRIPT
$QOSMATE_AUTORATE_TC_SCRIPT
$QOSMATE_AUTORATE_SERVICE
/usr/lib/tc/experimental.dist
/usr/lib/tc/normal.dist
/usr/lib/tc/normmix20-64.dist
/usr/lib/tc/pareto.dist
/usr/lib/tc/paretonormal.dist"
QOSMATE_EXEC_FILES_BACKEND="
$QOSMATE_SERVICE_PATH
$QOSMATE_MAIN_SCRIPT
$QOSMATE_AUTORATE_SCRIPT
$QOSMATE_AUTORATE_SERVICE"
QOSMATE_EXTRA_FILES_BACKEND="" # might be useful in the future?
### Frontend files
### !!! When adding or removing frontend (or other) files which need path fixups,
### remember to update appropriate fixup files
QOSMATE_FRONTEND_DIR_JS=/www/luci-static/resources/view/qosmate
# stores the version and update channel
QOSMATE_VERSION_FILE_FRONTEND="${QOSMATE_FRONTEND_DIR_JS}/settings.js"
QOSMATE_FILE_TYPES_FRONTEND="GEN EXTRA JS" # change the value if adding more types
QOSMATE_GEN_FILES_FRONTEND="
/usr/share/luci/menu.d/luci-app-qosmate.json
/usr/share/rpcd/acl.d/luci-app-qosmate.json
/usr/libexec/rpcd/luci.qosmate
/usr/libexec/rpcd/luci.qosmate_stats"
QOSMATE_EXTRA_FILES_FRONTEND="" # might be useful in the future?
QOSMATE_JS_FILENAMES="
settings.js
hfsc.js
cake.js
advanced.js
rules.js
ratelimits.js
connections.js
custom_rules.js
ipsets.js
statistics.js"
# generate js file list with paths
QOSMATE_JS_FILES_FRONTEND=
for js_file in ${QOSMATE_JS_FILENAMES}; do
QOSMATE_JS_FILES_FRONTEND="${QOSMATE_JS_FILES_FRONTEND}${QOSMATE_FRONTEND_DIR_JS}/${js_file}${_NL_}"
done
QOSMATE_EXEC_FILES_FRONTEND="
/usr/libexec/rpcd/luci.qosmate
/usr/libexec/rpcd/luci.qosmate_stats"
# Silence shellcheck unused vars warnings
: "$START" "$STOP" "$USE_PROCD" "$VERSION" "$UPD_CHANNEL" "$EXTRA_COMMANDS" "$EXTRA_HELP" "$REQUIRED_PACKAGES" \
"$MAIN_BRANCH_FRONTEND" "$QOSMATE_FILES_REG_PATH_BACKEND" "$QOSMATE_FILES_REG_PATH_FRONTEND" \
"$QOSMATE_VERSION_FILE_BACKEND" "$QOSMATE_FILE_TYPES_BACKEND" "$QOSMATE_GEN_FILES_BACKEND" \
"$QOSMATE_EXEC_FILES_BACKEND" "$QOSMATE_EXTRA_FILES_BACKEND" "$QOSMATE_FILE_TYPES_FRONTEND" \
"$QOSMATE_GEN_FILES_FRONTEND" "$QOSMATE_EXTRA_FILES_FRONTEND" "$QOSMATE_EXEC_FILES_FRONTEND" \
"$QOSMATE_AUTORATE_SCRIPT" "$QOSMATE_AUTORATE_TC_SCRIPT" "$QOSMATE_AUTORATE_SERVICE" \
"${global_enabled:=}" "${ROOT_QDISC:=}" "${gameqdisc:=}"
### Utility functions
uci_tmp() {
uci -c "$TMP_CFG_DIR" "$@"
}
commit_tmp_config() {
uci_tmp commit qosmate &&
cp "$TMP_CFG_FILE" "$QOSMATE_CFG_FILE"
local rv=$?
rm -rf "$TMP_CFG_DIR"
[ $rv = 0 ] || error_out "Failed to save the new config."
return $rv
}
# 1 - string
# 2 - path to file
write_str_to_file() {
printf '%s\n' "$1" > "$2" || { error_out "Failed to write to file '$2'."; return 1; }
:
}
# 0 - (optional) '-p'
# 1 - path
try_mkdir() {
local IFS="$DEFAULT_IFS" p=
[ "$1" = '-p' ] && { p='-p'; shift; }
[ -d "$1" ] && return 0
mkdir ${p} "$1" || { error_out "Failed to create directory '$1'."; return 1; }
:
}
check_util() { command -v "$1" 1>/dev/null; }
error_out() { log_msg -err "${@}"; }
# prints each argument to a separate line
print_msg() {
local _arg
for _arg in "$@"
do
case "${_arg}" in
'') printf '\n' ;; # print out empty lines
*) printf '%s\n' "${_arg}"
esac
done
:
}
# logs each argument separately and prints to a separate line
# optional arguments: '-err', '-warn' to set logged error level
log_msg() {
local msgs_prefix='' _arg err_l=info msgs_dest
local IFS="$DEFAULT_IFS"
for _arg in "$@"
do
case "${_arg}" in
"-err") err_l=err msgs_prefix="Error: " ;;
"-warn") err_l=warn msgs_prefix="Warning: " ;;
'') printf '\n' ;; # print out empty lines
*)
case "$err_l" in
err|warn) msgs_dest="/dev/stderr" ;;
*) msgs_dest="/dev/stdout"
esac
printf '%s\n' "${msgs_prefix}${_arg}" > "$msgs_dest"
logger -t qosmate -p user."$err_l" "${msgs_prefix}${_arg}"
msgs_prefix=''
esac
done
:
}
# check if var names are safe to use with eval
are_var_names_safe() {
local var_name
for var_name in "$@"; do
case "$var_name" in *[!a-zA-Z_]*) error_out "Invalid var name '$var_name'."; return 1; esac
done
:
}
### Update-related functions
# 1 - component: <BACKEND|FRONTEND>
# return codes:
# 0 - OK
# 1 - general error
# 2 - missing files
# 3 - missing reg file
# 4 - non-matching md5sums
check_files_integrity() {
local files_missing='' reg_missing='' reg_file file files file_types component="$1"
eval "file_types=\"\${QOSMATE_FILE_TYPES_${component}}\"
reg_file=\"\${QOSMATE_FILES_REG_PATH_${component}}\""
[ -f "$reg_file" ] || reg_missing=1
files="$(print_file_list "$component" ALL)" && [ -n "$files" ] ||
{ error_out "Failed to get file list for component '$component'."; return 1; }
local IFS="$_NL_"
for file in $files; do
[ -z "$file" ] || [ -f "$file" ] && continue
error_out "Missing file: '$file'."
files_missing=1
done
IFS="$DEFAULT_IFS"
[ -z "$files_missing" ] || return 2
[ -z "$reg_missing" ] || { error_out "$reg_file is not found. Can not check files integrity."; return 3; }
md5sum -c "$reg_file" &>/dev/null || return 4
:
}
# 1 - component: BACKEND|FRONTEND
# 2 - types: 'ALL' (doesn't print executable files) or any combination (space-separated) of 'GEN', 'EXTRA', 'JS', 'EXEC'
print_file_list() {
local me=print_file_list file_type files='' \
component="$1" file_types="$2"
[ -n "$1" ] && [ -n "$2" ] || { error_out "$me: missing args."; return 1; }
case "$component" in
BACKEND|FRONTEND) ;;
*) error_out "$me: invalid component '$component'."; return 1
esac
if [ "$file_types" = ALL ]; then
eval "file_types=\"\${QOSMATE_FILE_TYPES_${component}}\""
fi
for file_type in ${file_types}; do
case "$file_type" in
GEN|EXTRA|JS|EXEC)
eval "files=\"${files}\${QOSMATE_${file_type}_FILES_${component}}${_NL_}\"" ;;
*) error_out "$me: invalid type '$file_type'"; return 1
esac
done
# remove extra newlines, leading and trailing whitespaces and tabs
printf '%s\n' "$files" | sed "s/^\s*//;s/\s*$//;/^$/d"
:
}
# When updating, old version of the script should call post_update_[X] functions from the new version
# post_update_1 should be called via '/bin/sh <qosmate_init_preinst_path> post_update_1' *before* new version is installed
# post_update_2 should be called via '/bin/sh "$QOSMATE_SERVICE_PATH" post_update_2' *after* new version is installed
# This allows flexibility when adding new features etc.
post_update_1() {
:
# <do something...>
return $?
}
post_update_2() {
# <do something...>
migrate_config
return $?
}
# Fetches qosmate distribution for specified component
# 1 - component (BACKEND|FRONTEND)
# 2 - tarball url
fetch_qosmate_component() {
[ -n "$1" ] && [ -n "$2" ] || { error_out "fetch_qosmate_component: missing arguments."; return 1; }
local component="$1" fetch_tarball_url="$2"
local fetch_rv extract_dir upd_dir="${QOSMATE_UPD_DIR}/${component}"
local tarball="${upd_dir}/remote_qosmate.tar.gz" ucl_err_file="${upd_dir}/ucl_err"
case "$component" in
BACKEND) repo_name=qosmate ;;
FRONTEND) repo_name=luci-app-qosmate ;;
*) error_out "$me: invalid component '$component'."; return 1
esac
rm -f "$ucl_err_file" "${tarball}"
rm -rf "${upd_dir}/${QOSMATE_REPO_AUTHOR}-${repo_name}-"*
try_mkdir -p "$upd_dir" || return 1
uclient-fetch "$fetch_tarball_url" -O "${tarball}" 2> "$ucl_err_file" &&
grep -q "Download completed" "$ucl_err_file" &&
tar -C "${upd_dir}" -xzf "${tarball}" &&
extract_dir="$(find "${upd_dir}/" -type d -name "${QOSMATE_REPO_AUTHOR}-${repo_name}-*")" &&
[ -n "$extract_dir" ] && [ "$extract_dir" != "/" ]
fetch_rv=${?}
rm -f "${tarball}"
[ "$fetch_rv" != 0 ] && [ -s "$ucl_err_file" ] &&
log_msg "uclient-fetch output: ${_NL_}$(cat "$ucl_err_file")."
rm -f "$ucl_err_file"
[ "$fetch_rv" = 0 ] && {
mv "${extract_dir:-?}"/* "${upd_dir:-?}/" ||
{ rm -rf "${extract_dir:-?}"; error_out "Failed to move files to dist dir."; return 1; }
}
rm -rf "${extract_dir:-?}"
return $fetch_rv
}
# Get GitHub ref and tarball url for specified component, update channel, branch and version
# 1 - component (BACKEND|FRONTEND)
# 2 - update channel: release|snapshot|branch=<github_branch>|commit=<commit_hash>
# 3 - version (optional): [qosmate_version|commit_hash]
# Output via variables:
# $4 - github ref (version/commit hash), $5 - tarball url, $6 - version type ('version' or 'commit')
get_gh_ref_data() {
set_res_vars() {
if [ "$gh_channel" = release ]; then
version="${gh_ref#v}"
else
version="$gh_ref"
fi
eval "$4"='$version' "$5"='${gh_url_api}/tarball/${gh_ref}' "$6"='$gh_ver_type' \
"${component}_prev_ref"='$gh_ref' "${component}_prev_ver_type"='$gh_ver_type' \
"${component}_prev_upd_channel"='$gh_channel' "${component}_prev_version"='$version'
}
get_and_process_ref()
{
local branch \
channel="$1" ptrn="$2" branches="$3" main_branch="$4" err_file="$5"
case "${channel}" in
release)
uclient-fetch "${gh_url_api}/releases" -O- 2> "$err_file" | {
jsonfilter -e '@[@.prerelease=false]' |
jsonfilter -a -e "@[@.target_commitish=\"${main_branch}\"].tag_name"
cat 1>/dev/null
} ;;
snapshot|branch=*|commit=*)
for branch in ${branches}
do
uclient-fetch "${gh_url_api}/commits?sha=${branch}" -O- 2> "$err_file" | {
jsonfilter -e '@[@.commit]["url"]' |
sed 's/.*\///' # only leave the commit hash
cat 1>/dev/null
}
done
esac |
if [ -n "${ptrn}" ]
then
grep "${ptrn}"
else
head -n1 # get latest version or commit
cat 1>/dev/null
fi
}
local branches='' main_branch grep_ptrn='' \
gh_ref='' gh_ver_type='' gh_url_api ref_fetch_tmp_dir="/tmp/qosmate-gh" ref_fetch_rv=0 \
prev_ref prev_ver_type prev_upd_channel prev_version \
component="$1" gh_channel="$2" version="$3"
[ "$gh_channel" = release ] && version="${version#v}"
local ref_ucl_err_file="${ref_fetch_tmp_dir}/ucl_err"
are_var_names_safe "$4" "$5" "$6" || return 1
eval "$4='' $5='' $6='' gh_url_api=\"\${QOSMATE_GH_API_URL_${component}}\""
eval "prev_ref=\"\${${component}_prev_ref}\"
prev_ver_type=\"\${${component}_prev_ver_type}\"
prev_upd_channel=\"\${${component}_prev_upd_channel}\"
prev_version=\"\${${component}_prev_version}\""
# if previously stored data exists, use it without API query or cache check
if [ -n "$prev_ref" ] && [ -n "$prev_ver_type" ] && \
[ "$prev_upd_channel" = "$gh_channel" ] && [ "$version" = "$prev_version" ]; then
gh_ref="$prev_ref" gh_ver_type="$prev_ver_type"
else
# if commit hash is specified and it's 40-char long, use it directly without API query or cache check
case "$gh_channel" in
snapshot|branch=*|commit=*) [ "${#version}" = 40 ] && gh_ref="$version"
esac
if [ -z "$gh_ref" ]; then
# ref cache
local cache_file cache_filename="${component}_${version}_${gh_channel}" ref_cache_dir="/tmp/qosmate_cache" cache_ttl
case "$gh_channel" in
commit=*) cache_ttl=2880 ;; # 48 hours
*) cache_ttl=10 # 10 minutes
esac
# clean up old cache
find "${ref_cache_dir:-?}" -maxdepth 1 -type f -mmin +"${cache_ttl}" -exec rm -f {} \; 2>/dev/null
# check if the query is cached
cache_file="$(find "${ref_cache_dir:-?}" -maxdepth 1 -type f -name "${cache_filename}" -print 2>/dev/null)"
case "$cache_file" in
'') ;; # found nothing
*[^"$_NL_"]*"${_NL_}"*[^"${_NL_}"]*)
# found multiple files - delete them
local file IFS="$_NL_"
for file in $cache_file; do
[ -n "$file" ] || continue
rm -f "$file"
done
IFS="$DEFAULT_IFS" ;;
*)
# found cached query
if [ -z "$IGNORE_CACHE" ] && [ -f "$cache_file" ] && read -r prev_ref prev_ver_type < "$cache_file" &&
[ -n "$prev_ref" ] && [ -n "$prev_ver_type" ]; then
gh_ref="$prev_ref" gh_ver_type="$prev_ver_type"
else
rm -f "${cache_file:-???}"
fi
esac
fi
fi
if [ -n "$gh_ref" ]; then
set_res_vars "$@"
return 0
fi
try_mkdir -p "$ref_fetch_tmp_dir" || return 1
rm -f "$ref_ucl_err_file"
eval "main_branch=\"\${MAIN_BRANCH_${component}}\""
case "$gh_channel" in
release)
gh_ver_type=version
[ -n "$version" ] && grep_ptrn="^v${version}$" ;;
snapshot)
gh_ver_type=commit
branches="$main_branch"
if [ -n "$version" ]; then
grep_ptrn="^${version}$"
fi ;;
branch=*)
gh_ver_type=commit
branches="${gh_channel#*=}"
if [ -n "$version" ]; then
grep_ptrn="^${version}$"
fi ;;
commit=*)
gh_ver_type=commit
local gh_hash="${gh_channel#*=}"
if [ "${#gh_hash}" = 40 ]; then
# if upd. ch. is 'commit', the upd. ch. string includes commit hash -
# if it's 40-char long, use it directly without API query
gh_ref="$gh_hash"
else
branches="$(
uclient-fetch "${gh_url_api}/branches" -O- 2> "$ref_ucl_err_file" |
{ jsonfilter -e '@[@]["name"]'; cat 1>/dev/null; }
)"
[ -n "$branches" ] || {
error_out "Failed to get $component branches via GH API (url: '${gh_url_api}/branches')."
[ -f "$ref_ucl_err_file" ] &&
log_msg "uclient-fetch log:${_NL_}$(cat "$ref_ucl_err_file")"
rm -f "$ref_ucl_err_file"
return 1
}
rm -f "$ref_ucl_err_file"
grep_ptrn="^${gh_hash}"
fi ;;
*)
error_out "Invalid update channel '$gh_channel'."
return 1
esac
# Get ref via GH API
[ -z "$gh_ref" ] && gh_ref="$(get_and_process_ref "$gh_channel" "$grep_ptrn" "$branches" "$main_branch" "$ref_ucl_err_file")"
if [ -z "$gh_ref" ] && [ -f "$ref_ucl_err_file" ] && ! grep -q "Download completed" "$ref_ucl_err_file"; then
error_out "Failed to get $component GitHub download URL for $gh_ver_type '$version' (update channel: '$gh_channel')." \
"uclient-fetch log:${_NL_}$(cat "$ref_ucl_err_file")"
ref_fetch_rv=1
fi
rm -rf "${ref_fetch_tmp_dir:-?}"
[ "$ref_fetch_rv" = 0 ] || return 1
# validate resulting ref
case "$gh_ref" in
*[^"$_NL_"]*"${_NL_}"*[^"${_NL_}"]*)
error_out "Got multiple $component download URLs for version '$version'." \
"If using commit hash, please specify the complete commit hash string."
return 1 ;;
''|*[!a-zA-Z0-9._-]*)
error_out "Failed to get $component GitHub download URL for $gh_ver_type '$version' (update channel: '$gh_channel')."
return 1
esac
# write the query result to cache
try_mkdir -p "$ref_cache_dir" &&
printf '%s\n' "$gh_ref $gh_ver_type" > "${ref_cache_dir}/${cache_filename}"
set_res_vars "$@"
:
}
# get update channel and version from local frontend file
# 1 - var name for version output
# 2 - var name for upd. channel output
# 3 - path to file
get_frontend_spec() {
local me=get_frontend_spec rv=0 failed_spec='' spec_res='' spec_line
spec_path="$3"
are_var_names_safe "$1" "$2" || return 1
[ -n "$3" ] || { error_out "$me: missing args."; return 1; }
# assumes string enclosed in FS and no prior FS present in the line
spec_res="$(
awk -v v_ptrn_1="^[ ]*const UI_VERSION[ ]*=" -v v_ptrn_2="^[-a-zA-Z0-9_.]+$" \
-v u_ptrn_1="^[ ]*const UI_UPD_CHANNEL[ ]*=" -v u_ptrn_2="^[-a-zA-Z0-9_.=]+$" \
-F "'" '
BEGIN{rv=1}
{
if (v_match_res != "" && u_match_res != "") {rv = v_match_res + u_match_res; exit}
}
$0~v_ptrn_1 {
v_match_res=2
if ( $2~v_ptrn_2 ) {print "version:" $2; v_match_res=0}
next
}
$0~u_ptrn_1 {
u_match_res=3
if ( $2~u_ptrn_2 ) {print "upd_channel:" $2; u_match_res=0}
next
}
END{exit rv}
' "$spec_path"
)" || {
rv=$?
case $rv in
2) failed_spec=version ;;
3) failed_spec="update channel" ;;
*) failed_spec="version and update channel" ;;
esac
error_out "$me: Failed to get frontend $failed_spec from file '$spec_path'."
}
local IFS="$_NL_"
for spec_line in $spec_res; do
case "$spec_line" in
'') continue ;;
version:*) eval "$1"='${spec_line#*:}' ;;
upd_channel:*) eval "$2"='${spec_line#*:}' ;;
*) error_out "$me: got unexpected string when parsing file '$spec_path'."; return 1
esac
done
return $rv
}
# Get version and update channel from local file or from repo
# 1 - var name for version output
# 2 - var name for update channel output
# 3 - component (BACKEND|FRONTEND)
# 4 - origin (local|remote)
# Error codes: 1 - failed to get version, 2 - got invalid version
get_component_spec() {
local gv_version='' gv_upd_channel='' me=get_component_spec \
gv_component="$3" gv_origin="$4"
are_var_names_safe "$1" "$2" || return 1
# get update channel
case "$gv_component" in
BACKEND) gv_upd_channel="$UPD_CHANNEL" ;;
FRONTEND) get_frontend_spec gv_version gv_upd_channel "$QOSMATE_VERSION_FILE_FRONTEND" || return 1 ;;
*) error_out "$me: invalid component '$gv_component'."; return 1
esac
# get version
case "$gv_origin" in
local) [ "$gv_component" = BACKEND ] && gv_version="$VERSION" ;;
remote) get_gh_ref_data "$gv_component" "$gv_upd_channel" "" gv_version _ _ || return 1 ;;
*) error_out "$me: invalid origin '$gv_origin'."; return 1
esac
: "$gv_version" # Silence shellcheck warning
eval "$1"='$gv_version'
eval "$2"='$gv_upd_channel'
:
}
# Check and print versions for local and remote backend and frontend
# sets global variables: $BACKEND_upd_avail, $FRONTEND_upd_avail
# (optional) '-n' to not print update tip
# (optional) '-i' to ignore cache
# (optional) '-c <BACKEND|FRONTEND>''
# Return codes:
# 0 - no update
# 1 - error
# 254 - update available
check_version() {
local origin notify_origin components='' components_arg='' component notify_component cv_version cv_upd_channel local_version \
upd_avail='' no_print_tip=
while getopts ":c:ni" opt; do
case ${opt} in
c) components_arg=$OPTARG ;;
n) no_print_tip=1 ;;
i) IGNORE_CACHE=1 ;; # global var
*) ;;
esac
done
components="${components_arg:-"$QOSMATE_COMPONENTS"}"
for component in $components; do
case "$component" in
BACKEND) notify_component=Backend ;;
FRONTEND) notify_component=Frontend ;;
*) error_out "check_version: invalid component '$component'."; return 1
esac
print_msg "$notify_component versions:"
local upd_ch_printed=
for origin in local remote; do
case "$origin" in
local) notify_origin=Current ;;
remote) notify_origin=Latest
esac
get_component_spec cv_version cv_upd_channel "$component" "$origin" || {
error_out "Failed to get $origin $component version." \
"To force re-installation of $component, use the command 'service qosmate update -f -c $component'."
return 1
}
[ -z "$upd_ch_printed" ] && { print_msg " Update channel: $cv_upd_channel"; upd_ch_printed=1; }
case "$origin" in
local) local_version="$cv_version" ;;
remote)
if [ "$cv_version" = "$local_version" ]; then
unset "${component}_upd_avail"
else
upd_avail=1
eval "${component}_upd_avail=1"
fi
esac
printf '%s\n' " $notify_origin version: $cv_version"
done
done
if [ -n "$upd_avail" ]; then
[ -z "$no_print_tip" ] && printf '\n%s\n%s\n' "A new version of QoSmate is available." \
"To update, run: /etc/init.d/qosmate update"
return 254
else
printf '\n%s\n' "QoSmate components '$components' are up to date."
fi
:
}
# Optional args:
# -s <path> : simulate update (intended for testing: service qosmate update -s <path_to_new_ver> -v <version>)
# -c <frontend|backend> : only update specified component
# -v [<version>|package[=<version>]|release|snapshot|branch=<branch>|commit=<commit_hash>] : version string
# -U <update_channel> : force this update channel (overrides the upd. channel derived from '-v' option)
# -W <version> : force this version (overrides the version derived from '-v' option)
# -f : force update
# -i : ignore previous cache results
update() {
upd_failed() {
rm -rf "${QOSMATE_UPD_DIR:-?}"
[ -n "$*" ] && error_out "$@"
error_out "Failed to update QoSmate."
}
pkg_update_not_impl() {
upd_failed "Update channel 'package' not implemented."
}
# 1 - fixup type: <file|dir>
# 2 - path to fixup file
# 3 - distribution dir
fixup_paths() {
local fixup_line='' fetched_path dest_path me=fixup_paths \
fixup_type="$1" fixup_file="$2" dist_dir="$3"
[ -n "$1" ] && [ -n "$2" ] && [ -n "$3" ] || { error_out "$me: missing args."; return 1; }
while IFS='' read -r fixup_line || [ -n "$fixup_line" ]; do
case "$fixup_line" in
"#"*) continue ;; # skip comments
*=*)
fetched_path="${fixup_line%%=*}"
fetched_path="${fetched_path%/}"
dest_path="${fixup_line#*=}"
dest_path="${dest_path%/}"
[ -n "$fetched_path" ] && [ "$fetched_path" != "/" ] && [ -n "$dest_path" ] ||
{ error_out "$me: invalid line in fixup file: '$fixup_line'."; return 1; }
# warn about and skip non-existing files and directories
[ -e "${dist_dir:-???}${fetched_path:-???}" ] ||
{ log_msg -warn "$me: path '${dist_dir}${fetched_path}' does not exist."; continue; }
case "$fixup_type" in
dir)
try_mkdir -p "${dist_dir}${dest_path}" &&
mv "${dist_dir:-???}${fetched_path:-???}"/* "${dist_dir:-???}${dest_path:-???}/" ;;
file)
try_mkdir -p "${dist_dir}${dest_path%/*}" &&
mv "${dist_dir:-???}${fetched_path:-???}" "${dist_dir}${dest_path}" ;;
*) error_out "$me: invalid fixup type '$fixup_type'."; return 1
esac || {
error_out "Failed to move $fixup_type '${dist_dir}${fetched_path}' to '${dist_dir}${dest_path}'."
return 1
} ;;
*) continue
esac
done < "$fixup_file" || return 1
:
}
unexp_arg() { upd_failed "update: unexpected argument '$1'."; }
local file origin new_file_list exec_files sim_path='' req_ver='' ver_type ver_str_arg='' \
extract_dir='' dist_dir='' upd_version='' tarball_url='' file_list_query_path \
backend_upd_req='' upd_component_arg='' upd_components='' req_upd_components='' \
req_upd_channel upd_channel='' def_upd_channel='' force_upd_channel='' force_ver='' force_update=''
IGNORE_CACHE=
while getopts ":s:v:c:U:W:fi" opt; do
case ${opt} in
c) upd_component_arg=$OPTARG ;;
s) sim_path=$OPTARG ;;
v) ver_str_arg=$OPTARG force_update=1 ;;
U) force_upd_channel=$OPTARG force_update=1 ;;
W) force_ver=$OPTARG force_update=1 ;;
f) force_update=1 ;;
i) IGNORE_CACHE=1 ;; # global var
*) unexp_arg "$OPTARG"; return 1
esac
done
shift $((OPTIND-1))
[ -z "${*}" ] || { unexp_arg "${*}"; return 1; }
case "$upd_component_arg" in
'') ;;
backend|BACKEND) upd_component_arg=BACKEND ;;
frontend|FRONTEND) upd_component_arg=FRONTEND ;;
*) upd_failed "Unexpected component '$upd_component_arg'"; return 1
esac
req_upd_components="${upd_component_arg:-"$QOSMATE_COMPONENTS"}"
if [ -n "$force_update" ]; then
upd_components="$req_upd_components"
else
check_version -n -c "$req_upd_components"
case $? in
0) return 0 ;;
1) return 1 ;;
254)
print_msg "Updates available. Do you want to update? [y/N] "
read -r answer
case "$answer" in
y|Y) ;;
*)
print_msg "Update cancelled."
return 0
esac
esac
for component in $req_upd_components; do
eval "[ -n \"\${${component}_upd_avail}\" ]" && upd_components="${upd_components}${component} "
done
fi
log_msg "Updating QoSmate components '$upd_components'..."
# parse version string from arguments into $req_upd_channel, $req_ver
case "$ver_str_arg" in
'') ;;
package*)
pkg_update_not_impl
return 1 ;;
release)
req_upd_channel="${ver_str_arg}" req_ver='' ;;
snapshot)
req_upd_channel="${ver_str_arg}" req_ver='' ;;
commit=*)
req_upd_channel="${ver_str_arg}" req_ver="${ver_str_arg#*=}" ;;
branch=*)
req_upd_channel="$ver_str_arg" req_ver='' ;;
[0-9]*|v[0-9]*)
req_upd_channel=release
req_ver="${ver_str_arg#*=}"
req_ver="${req_ver#v}"
;;
*)
upd_failed "Invalid version string '$ver_str_arg'."
return 1
esac
req_upd_channel="${force_upd_channel:-"${req_upd_channel}"}"
req_ver="${force_ver:-"${req_ver}"}"
# updating multiple components to same commit hash makes no sense
case "$req_upd_channel" in
snapshot|branch=*|commit=*)
case "$upd_components" in BACKEND*FRONTEND|FRONTEND*BACKEND)
[ -n "$req_ver" ] && {
upd_failed "Can not update multiple components '$upd_components' to version '$req_ver'."
return 1
}
esac
esac
rm -rf "${QOSMATE_UPD_DIR:-?}"
try_mkdir -p "$QOSMATE_UPD_DIR" || { upd_failed; return 1; }
if [ -n "$sim_path" ]
then
log_msg "Updating in simulation mode."
[ -d "$sim_path" ] || { upd_failed "Update simulation directory '$sim_path' does not exist."; return 1; }
[ -n "${req_ver}" ] || { upd_failed "Specify new version."; return 1; }
def_upd_channel=release
upd_version="${req_ver}"
: "${req_upd_channel:="${def_upd_channel}"}"
for component in $upd_components; do
[ -d "${sim_path}/${component}" ] ||
{ upd_failed "Simulation source directory doesn't have ${component} directory"; return 1; }
done
cp -rT "$sim_path" "$QOSMATE_UPD_DIR"
fi
case "$upd_components" in *BACKEND*)
backend_upd_req=1
esac
for component in $upd_components; do
upd_channel=
# set default update channel
case "$component" in
BACKEND)
def_upd_channel="${UPD_CHANNEL:-release}" ;;
FRONTEND)
if [ -n "$req_upd_channel" ]; then
:
elif [ ! -f "$QOSMATE_VERSION_FILE_FRONTEND" ]; then
def_upd_channel="${UPD_CHANNEL:-release}"
else
get_frontend_spec _ def_upd_channel "$QOSMATE_VERSION_FILE_FRONTEND" || {
error_out "Failed to get current FRONTEND update channel. Defaulting to 'release'."
def_upd_channel=release
}
fi
esac
upd_channel="${req_upd_channel:-"${def_upd_channel}"}"
dist_dir="${QOSMATE_UPD_DIR}/${component}"
case "$upd_channel" in
package)
pkg_update_not_impl
return 1 ;;
*)
if [ -n "$sim_path" ]
then
log_msg "" "Updating $component to version '$upd_version' (update channel: '$upd_channel')."
else
get_gh_ref_data "$component" "$upd_channel" "$req_ver" upd_version tarball_url ver_type || return 1
case "$upd_channel" in
commit=*)
# set update channel to 'commit=<full_commit_hash>'
upd_channel="${upd_channel%=*}=${upd_version}"
esac
log_msg "" "Downloading $component, $ver_type '$upd_version' (update channel: '$upd_channel')."
fetch_qosmate_component "$component" "$tarball_url" || { upd_failed; return 1; }
fi
if [ -n "$backend_upd_req" ]; then
file_list_query_path="${QOSMATE_UPD_DIR}/BACKEND${QOSMATE_SERVICE_PATH}"
else
file_list_query_path="${QOSMATE_SERVICE_PATH}"
fi
new_file_list="$(/bin/sh "$file_list_query_path" print_file_list "$component" ALL)" &&
write_str_to_file "$new_file_list" "${dist_dir}/new_file_list" &&
[ -n "$new_file_list" ] || {
upd_failed "Failed to get file list from the fetched QoSmate version." \
"NOTE: QoSmate versions prior to v1.2.0 do not support the new update mechanism."
return 1
}
exec_files="$(/bin/sh "$file_list_query_path" print_file_list "$component" EXEC)" &&
write_str_to_file "$exec_files" "${dist_dir}/exec_files" || { upd_failed; return 1; }
eval "ver_${component}"='$upd_version'
eval "upd_channel_${component}"='$upd_channel'
# fix-up paths if needed
local dir_fixup_file="${dist_dir}/dir_fixups.txt"
local path_fixup_file="${dist_dir}/file_fixups.txt"
if [ -s "$dir_fixup_file" ]; then
fixup_paths "dir" "$dir_fixup_file" "$dist_dir" || { upd_failed "Failed to fix-up dir paths."; return 1; }
fi
if [ -s "$path_fixup_file" ]; then
fixup_paths "file" "$path_fixup_file" "$dist_dir" || { upd_failed "Failed to fix-up file paths."; return 1; }
fi
esac
done
[ -n "$backend_upd_req" ] &&
/bin/sh "${QOSMATE_UPD_DIR}/BACKEND${QOSMATE_SERVICE_PATH}" post_update_1
case "$ver_str_arg" in
package*)
pkg_update_not_impl
return 1 ;;
*)
install_qosmate_files "$QOSMATE_UPD_DIR" "$upd_components" || { upd_failed; return 1; }
rm -rf "${QOSMATE_UPD_DIR:-?}"
esac
[ -n "$backend_upd_req" ] && chmod +x "$QOSMATE_SERVICE_PATH"
/bin/sh "$QOSMATE_SERVICE_PATH" post_update_2 # post_update_2 is called when updating either component
log_msg "QoSmate components '$upd_components' have been successfully updated."
if [ -n "$backend_upd_req" ] && "$QOSMATE_SERVICE_PATH" enabled; then
log_msg "" "Restarting QoSmate."
${QOSMATE_SERVICE_PATH} restart
fi
:
}
# 1 - path to upper distribution dir (containing a dir for each component)
# 2 - component(s): <BACKEND|FRONTEND|"BACKEND FRONTEND">
install_qosmate_files() {
inst_failed() {
[ -n "$1" ] && error_out "$1"
error_out "Failed to install new $component files."
}
local file preinst_path curr_files new_file_list new_file_list exec_files='' \
dist_dir ver_file_path frontend_updated='' \
upper_dist_dir="$1" components="$2" version upd_channel
for component in $components; do
log_msg "" "Installing new $component files..."
eval "version=\"\${ver_${component}}\" upd_channel=\"\${upd_channel_${component}}\""
[ -n "$version" ] && [ -n "$upd_channel" ] ||
{ inst_failed "Internal error: failed to get version and update channel for component '$component'."; return 1; }
dist_dir="${upper_dist_dir}/${component}"
# read new file list
new_file_list="$(cat "${dist_dir}/new_file_list")" && [ -n "$new_file_list" ] &&
exec_files="$(cat "${dist_dir}/exec_files")" ||
{
rm -f "${dist_dir}/new_file_list" "${dist_dir}/exec_files"
inst_failed "Failed to read new file list."
return 1
}
rm -f "${dist_dir}/exec_files"
# get current file list
curr_files="$(print_file_list "$component" ALL)" || { inst_failed; return 1; }
eval "ver_file_path=\"\${QOSMATE_VERSION_FILE_${component}}\""
# version and update channel string replacement vars
local ver_ptrn_prefix ver_repl_str upd_ch_repl_str
case "$component" in
BACKEND)
ver_ptrn_prefix=
ver_repl_str="VERSION=\"$version\""
upd_ch_repl_str="UPD_CHANNEL=\"$upd_channel\"" ;;
FRONTEND)
ver_ptrn_prefix="const UI_"
ver_repl_str="VERSION = '$version';"
upd_ch_repl_str="UPD_CHANNEL = '$upd_channel';" ;;
esac
# set version and update channel in component's main file
local preinst_ver_file_path="${dist_dir}${ver_file_path}"
sed -i "
/^\s*${ver_ptrn_prefix}VERSION\s*=/{s/.*/${ver_ptrn_prefix}${ver_repl_str}/;}
/^\s*${ver_ptrn_prefix}UPD_CHANNEL\s*=/{s/.*/${ver_ptrn_prefix}${upd_ch_repl_str}/;}" \
"$preinst_ver_file_path" &&
# verify that substitution worked
grep -q "^${ver_ptrn_prefix}${ver_repl_str}" "$preinst_ver_file_path" &&
grep -q "^${ver_ptrn_prefix}${upd_ch_repl_str}" "$preinst_ver_file_path" ||
{ inst_failed "Failed to set version in file '$preinst_ver_file_path'."; return 1; }
# Check for changed files
local curr_reg_file changed_files='' unchanged_files='' man_changed_files='' \
prefixed_curr_reg_file="${dist_dir}/prefixed_reg_${component}.md5"
eval "curr_reg_file=\"\${QOSMATE_FILES_REG_PATH_${component}}\""
if [ -s "$curr_reg_file" ]; then
# prefix file paths in the reg file for md5sum comparison
sed -E "/^$/d;s~([^ ]+$)~${dist_dir}\\1~" "$curr_reg_file" > "$prefixed_curr_reg_file"
unchanged_files="$(md5sum -c "$prefixed_curr_reg_file" 2>/dev/null |
sed -n "/:\s*OK\s*$/{s/\s*:\s*OK\s*$//;s~^\s*${dist_dir}~~;p;}")"
rm -f "$prefixed_curr_reg_file"
# remove unchanged files from $new_file_list to reliably get a list of files to copy
changed_files="$(
printf '%s\n' "$unchanged_files" | awk '
NR==FNR {unch[$0];next}
($0=="" || $0 in unch) {next}
{print}
' - "${dist_dir}/new_file_list"
)"
# Detect manually modified files
man_changed_files="$(
md5sum -c "$curr_reg_file" 2>/dev/null |
sed -n "/:\s*FAILED\s*$/{s/\s*:\s*FAILED\s*$//;p;}" |
awk '
NR == FNR {new[$0]; next}
$0 in new {print}
' "${dist_dir}/new_file_list" -
)"
# Add manually modified files to changed files
if [ -n "$man_changed_files" ]; then
changed_files="$(printf '%s\n' "${changed_files}${_NL_}${man_changed_files}" | sort -u | sed '/^$/d')"
fi
else
changed_files="$new_file_list"
fi
local IFS="$_NL_"
for file in $unchanged_files; do
[ -n "$file" ] || continue
log_msg "File '$file' did not change - not updating."
done
local mod_files_bk_dir="/tmp/qosmate_old_modified_files"
for file in $man_changed_files; do
[ -n "$file" ] && [ -f "$file" ] || continue
log_msg -warn "File '$file' was manually modified - overwriting."
if try_mkdir -p "$mod_files_bk_dir" && cp "$file" "${mod_files_bk_dir}/${file##*/}"; then
log_msg "Saved a backup copy of manually modified file to ${mod_files_bk_dir}/${file##*/}"
else
log_msg -warn "Can not create a backup copy of manually modified file '$file' - overwriting anyway."
fi
done
# Copy changed files
for file in $changed_files
do
preinst_path="${dist_dir}${file}"
log_msg "Copying file '${file}'."
try_mkdir -p "${file%/*}" && cp "$preinst_path" "$file" ||
{ inst_failed "Failed to copy file '$preinst_path' to '$file'."; return 1; }
[ "$component" = FRONTEND ] && frontend_updated=1
done
# delete obsolete files
for file in ${curr_files}
do
[ -f "$file" ] || continue
# check for $file in $new_file_list, allowing newline as list delimiter
case "$new_file_list" in
"$file"|"${file}${_NL_}"*|*"${_NL_}${file}"|*"${_NL_}${file}${_NL_}"*)
continue ;;
*)
log_msg "Deleting obsolete file '$file'."
rm -f "$file"
esac
done
# make files executable
set -- $exec_files # relying on IFS=\n
chmod +x "$@" || { inst_failed "Failed to make files executable."; return 1; }
# save the md5sum registry file if needed
if [ -n "$changed_files" ] || [ ! -s "$curr_reg_file" ]; then
# make md5sum registry of new files
# shellcheck disable=SC2046
set -- $(printf '%s\n' "$new_file_list" | sed "/^$/d;s~^\s*~${dist_dir}~") # relying on IFS=\n
md5sums="$(md5sum "$@")" && [ -n "$md5sums" ] &&
try_mkdir -p "${curr_reg_file%/*}" &&
printf '%s\n' "$md5sums" | sed "s~\s${dist_dir}~ ~" > "$curr_reg_file" ||
{ inst_failed "Failed to register new files."; return 1; }
fi
IFS="$DEFAULT_IFS"
done
# Restart rpcd only if any frontend files were updated
[ -n "$frontend_updated" ] && /etc/init.d/rpcd restart
:
}
### Config-related functions
preserve_config_files() {
local path save_req='' \
paths="$QOSMATE_MAIN_SCRIPT $QOSMATE_SERVICE_PATH $QOSMATE_HOTPLUG_SCRIPT $QOSMATE_D" \
tmp_file="/tmp/qosmate_sysupgr" \
sysupgr_file="/etc/sysupgrade.conf"
rm -f "$tmp_file"
printf '\n'
if [ "$PRESERVE_CONFIG_FILES" = 1 ]; then
for path in $paths; do
grep -qxF "$path" "$sysupgr_file" && continue
echo "$path" >> "$tmp_file" || return 1
save_req=1
done
if [ -n "$save_req" ]; then
cat "$tmp_file" >> "$sysupgr_file" &&
print_msg "Config files have been added to $sysupgr_file for preservation."
else
print_msg "$sysupgr_file already lists qosmate config files."
fi
else
print_msg "Preservation of config files is disabled."
# Remove the config files from sysupgrade.conf if they exist
[ -f "$sysupgr_file" ] || return 0
cp "$sysupgr_file" "$tmp_file" || return 1
for path in $paths; do
grep -qxF "$path" "$tmp_file" || continue
sed -i "\|^$path$|d" "$tmp_file" || return 1
save_req=1
done
if [ -n "$save_req" ]; then
mv "$tmp_file" "$sysupgr_file" &&
print_msg "Config files have been removed from $sysupgr_file."
else
print_msg "$sysupgr_file does not list qosmate config files."
fi
fi
rm -f "$tmp_file"
:
}
# return codes:
# 0 - OK
# 1 - error
# 154 - error in default config
# 155 - missing sections and/or options in user config
parse_config() {
local file miss_sec_f="/tmp/qosmate-missing-sections" miss_opt_f="/tmp/qosmate-missing-options"
for file in "$QOSMATE_CFG_FILE" "$QOSMATE_DEFAULTS_FILE"; do
[ -f "$file" ] || {
error_out "$file not found."
return 1
}
done
awk -v miss_sec_f="$miss_sec_f" -v miss_opt_f="$miss_opt_f" \
-v bad_quotes="(\"|'.*'.*')" -v p1="'[^']*$" -v p2=".*'" ' \
function get_val(s){
if (sub(p1,"",s) && sub(p2,"",s)) return s; else return "ERROR"
}
BEGIN{
rv=0
# Primary settings must be printed first
def_prim_arr["settings:UPRATE"]
def_prim_arr["settings:DOWNRATE"]
}
# serialize user config into user_sections_arr, user_opts_arr
NR == FNR {
if ($0 ~ bad_quotes) next # ignore lines with double-quotes or more than 2 single-quotes
if ( $0 ~ /^[ ]*config[ ]/ ) {
section = get_val($0)
if (section == "ERROR" || section !~ /^[a-zA-Z0-9_]+$/) next # ignore invalid section declarations
user_sections_arr[section]
next
}
if ( $0 ~ /^[ ]*option[ ]/ ) {
if ( ! section || $2 !~ /^[a-zA-Z0-9_]+$/ ) next # ignore options w/ invalid keys or outside section
val = get_val($0)
if ( val == "ERROR" || ! val ) next # ignore options w/ invalid or empty values
user_opts_arr[section ":" $2] = val
}
next
}
# serialize default config into def_prim_arr, def_secondary_arr
# and check against it for missing sections or options in user config
FNR == 1 { section = ""; section_ind = 0 }
$0 ~ /^[ ]*($|#)/ {next} # ignore comments and empty lines
/^[ ]*config[ ]/ {
if ($0 ~ bad_quotes) {rv=154;exit}
section = get_val($0)
if (section == "ERROR" || section !~ /^[a-zA-Z0-9_]+$/) {rv=154; exit}
if (section in user_sections_arr) {section_ind++;next}
# user config is missing section
missing_sections = missing_sections section "=" section_ind "\n"
print section >> miss_sec_f
section_ind++
next
}
/^[ ]*option[ ]/ {
if ($0 ~ bad_quotes) {rv=154;exit}
if ( ! section || $2 !~ /^[a-zA-Z0-9_]+$/ ) {rv=154; exit}
val = get_val($0)
if (val == "ERROR") {rv=154; exit}
ser_opt = section ":" $2
if (ser_opt in def_prim_arr) def_prim_arr[ser_opt] = val
else def_secondary_arr[ser_opt] = val
if (! val) next # do not check user opts vs def opts without value
if (ser_opt in user_opts_arr) next
# user config is missing option
missing_opts = missing_opts ser_opt "=" val "\n"
print ser_opt >> miss_opt_f
}
END{
if (rv == 1 || rv == 154) exit rv
if (missing_sections) {
rv = 155
print "missing_sections=\"" missing_sections "\""
}
if (missing_opts) {
rv = 155
print "missing_opts=\"" missing_opts "\""
}
for (ser_opt in def_prim_arr) {if (! def_prim_arr[ser_opt]) exit 154} # check that primary opts are set in def config
printf "%s", "ser_def_opts=\""
for (ser_opt in def_prim_arr) {print ser_opt "=" def_prim_arr[ser_opt]}
for (ser_opt in def_secondary_arr) print ser_opt "=" def_secondary_arr[ser_opt]
print "\""
exit rv
}' "$QOSMATE_CFG_FILE" "$QOSMATE_DEFAULTS_FILE"
rv=$?
[ $rv != 155 ] && { rm -f "$miss_sec_f" "$miss_opt_f"; return $rv; }
for file in "$miss_sec_f" "$miss_opt_f"; do
[ -f "$file" ] || continue
log_msg -warn "" "Missing config ${file##*-}:" "$(cat "$file")"
rm -f "$file"
done
return 155
}
load_and_fix_config() {
export QOSMATE_CONFIG_LOADED=
try_load_and_fix_config "$@" && { QOSMATE_CONFIG_LOADED=1; return 0; }
# 155 = config loaded but some options/sections missing
[ $? != 155 ] && {
error_out "Failed to load config."
uci revert qosmate 2>/dev/null
}
return 1
}
# return codes:
# 0 - check or fix OK
# 1 - error
try_load_and_fix_config() {
parse_ser_opt() {
case "$4" in
'') return 0 ;;
*:*=*) ;;
*) error_out "parse_ser_opt: unexpected input '$4'."; return 1
esac
local gso_section="${4%%:*}" gso_val="${4#*=}" gso_opt_name="${4#*:}"
gso_opt_name="${gso_opt_name%"=$gso_val"}"
eval "$1=\"$gso_section\" $2=\"$gso_opt_name\" $3=\"$gso_val\""
}
local nofix=
[ "$1" = '-nofix' ] && nofix=1
if [ ! -f "$QOSMATE_CFG_FILE" ]; then
[ -n "$nofix" ] && return 1
log_msg -warn "Config file not found, restoring from qosmate-defaults."
# qosmate-defaults is checked for and restored if required by start() using the update() routine
[ -f "$QOSMATE_DEFAULTS_FILE" ] || { error_out "$QOSMATE_DEFAULTS_FILE not found."; return 1; }
uci import qosmate < "$QOSMATE_DEFAULTS_FILE" || {
error_out "Failed to restore config."
return 1
}
log_msg "Config file restored."
fi
local IFS="$DEFAULT_IFS" section section_name ser_opt opt_name def_val \
parse_rv parse_res missing_sections='' missing_opts='' ser_def_opts=''
# Validate config and generate $parse_res for config repair
parse_res="$(parse_config)"
parse_rv=$?
case $parse_rv in
0|155) ;;
1) error_out "Failed to parse config."; return 1 ;;
154) error_out "Failed to process $QOSMATE_DEFAULTS_FILE."; return 1 ;;
*) error_out "Unexpected return code $parse_rv when parsing config."; return 1 ;;
esac
eval "$parse_res" || { error_out "Failed to parse config."; return 1; }
if [ "$parse_rv" = 155 ] && [ -z "$nofix" ]; then
log_msg "" "Adding missing sections and options from the default config."
IFS="$_NL_"
for section in $missing_sections; do
[ -n "$section" ] || continue
section_name="${section%%=*}"
index="${section#*=}"
log_msg "Adding config section '$section_name'."
uci set "qosmate.${section_name}=${section_name}" &&
uci reorder "qosmate.${section_name}=${index}" || return 1
done
for ser_opt in $missing_opts; do
[ -n "$ser_opt" ] || continue
parse_ser_opt section opt_name def_val "$ser_opt" || return 1
[ -n "$section" ] && [ -n "$opt_name" ] && [ -n "$def_val" ] || continue
log_msg "Adding option '$opt_name' to section '$section' with default value '$def_val'."
uci set "qosmate.${section}.${opt_name}=${def_val}" ||
{ error_out "Failed to add config option."; return 1; }
done
IFS="$DEFAULT_IFS"
uci commit qosmate || return 1
parse_rv=0
fi
config_load qosmate || return 1
IFS="$_NL_"
for ser_opt in $ser_def_opts; do
IFS="$DEFAULT_IFS"
parse_ser_opt section opt_name def_val "$ser_opt" || return 1
var_name="$opt_name"
# Var name and def val overrides
case "${section}:${opt_name}" in
global:enabled) var_name=global_enabled ;;
advanced:ACKRATE) def_val=$((UPRATE * 5 / 100)) ;;
hfsc:GAMEUP) def_val=$((UPRATE*15/100+400)) ;;
hfsc:GAMEDOWN) def_val=$((DOWNRATE*15/100+400)) ;;
hfsc:netem_direction) var_name=NETEM_DIRECTION ;;
# Autorate defaults (based on configured rates)
autorate:enabled) var_name=AUTORATE_ENABLED ;;
autorate:min_ul_rate) def_val=$((UPRATE * 25 / 100)); var_name=AUTORATE_MIN_UL ;;
autorate:base_ul_rate) def_val="$UPRATE"; var_name=AUTORATE_BASE_UL ;;
autorate:max_ul_rate) def_val=$((UPRATE * 105 / 100)); var_name=AUTORATE_MAX_UL ;;
autorate:min_dl_rate) def_val=$((DOWNRATE * 25 / 100)); var_name=AUTORATE_MIN_DL ;;
autorate:base_dl_rate) def_val="$DOWNRATE"; var_name=AUTORATE_BASE_DL ;;
autorate:max_dl_rate) def_val=$((DOWNRATE * 105 / 100)); var_name=AUTORATE_MAX_DL ;;
autorate:interval) var_name=AUTORATE_INTERVAL ;;
autorate:latency_increase_threshold) var_name=AUTORATE_LAT_INC_THR ;;
autorate:latency_decrease_threshold) var_name=AUTORATE_LAT_DEC_THR ;;
autorate:reflectors) var_name=AUTORATE_REFLECTORS ;;
autorate:refractory_increase) var_name=AUTORATE_REFRACT_INC ;;
autorate:refractory_decrease) var_name=AUTORATE_REFRACT_DEC ;;
autorate:adjust_up_factor) var_name=AUTORATE_ADJ_UP ;;
autorate:adjust_down_factor) var_name=AUTORATE_ADJ_DOWN ;;
esac
NO_EXPORT='' config_get "$var_name" "$section" "$opt_name" "$def_val"
done
IFS="$DEFAULT_IFS"
# Ensure valid values for DOWNRATE, UPRATE
case "$ROOT_QDISC" in hfsc|hybrid)
local opt val commit_req='' min_val=1000
for opt in DOWNRATE UPRATE; do
eval "val=\"\${${opt}}\""
if ! [ "$val" -gt 0 ]; then
log_msg -warn "$opt is '$val' for $ROOT_QDISC. Setting to minimum value of $min_val kbps."
eval "$opt=$min_val"
uci set "qosmate.settings.${opt}=${min_val}"
commit_req=1
fi
done
[ -z "$commit_req" ] || uci commit qosmate || return 1
esac
# Limit DOWNRATE based on BWMAXRATIO
if [ "$UPRATE" -gt 0 ] && [ $((DOWNRATE > UPRATE*BWMAXRATIO)) -eq 1 ]; then
print_msg "We limit the downrate to at most $BWMAXRATIO times the upstream rate to ensure no upstream ACK floods occur which can cause game packet drops"
DOWNRATE=$((BWMAXRATIO*UPRATE))
fi
return $parse_rv
}
migrate_config() {
try_migrate_config
local rv=$?
[ $rv = 0 ] || {
error_out "Failed to migrate config."
uci_tmp revert qosmate 2>/dev/null
}
rm -rf "$TMP_CFG_DIR"
return $rv
}
try_migrate_config() {
report_migration() {
local msg="Link layer settings migrated from settings for qdisc '$1' to advanced section: preset=$2"
[ -n "$3" ] && msg="$msg, overhead=$3"
log_msg "$msg"
}
delete_premigration_settings() {
local qdisc old_settings
for qdisc in cake hfsc; do
eval "old_settings=\"\${${qdisc}_old_settings}\""
for setting in $old_settings; do
uci_tmp -q delete "qosmate.$qdisc.$setting"
done
done
}
. /lib/functions.sh
local commit_req=''
[ -f "$QOSMATE_CFG_FILE" ] &&
try_mkdir -p "$TMP_CFG_DIR" &&
cp "$QOSMATE_CFG_FILE" "$TMP_CFG_FILE" || return 1
# Check for and correct the custom_rules section
awk "
BEGIN{cor=3; inc=3}
/^[ ]*config[ ]+qosmate[ ]+'custom_rules'[ ]*$/{inc=4}
/^[ ]*config[ ]+custom_rules[ ]+'custom_rules'[ ]*$/{cor=4}
END{exit cor inc}
" "$TMP_CFG_FILE"
case "$?" in
33)
# No custom_rules section found
;;
43)
# Only correct section found
;;
34)
# Only incorrect section found - correct it
commit_req=1
print_msg "Incorrect custom_rules section found. Correcting..."
sed -i "s/config qosmate 'custom_rules'/config custom_rules 'custom_rules'/" "$TMP_CFG_FILE"
print_msg "custom_rules section corrected."
;;
44)
# Both correct and incorrect sections found - remove duplicate
commit_req=1
print_msg "Both incorrect and correct custom_rules sections found. Removing the incorrect one..."
uci_tmp -q delete qosmate.@qosmate[0] || return 1
print_msg "Incorrect custom_rules section removed."
;;
*)
error_out "Unexpected return code '$?' when checking the 'custom_rules' section."
return 1
;;
esac
# Migrate link layer settings to advanced section based on active QDisc
local root_qdisc qdisc settings setting val adv_preset \
has_old_settings=0 \
cake_old_settings="COMMON_LINK_PRESETS OVERHEAD MPU LINK_COMPENSATION ETHER_VLAN_KEYWORD" \
hfsc_old_settings="LINKTYPE OH"
: "$cake_old_settings" "$hfsc_old_settings"
UCI_CONFIG_DIR="$TMP_CFG_DIR" config_load qosmate || return 1
config_get root_qdisc settings ROOT_QDISC
config_get adv_preset advanced COMMON_LINK_PRESETS
# Check for old settings, migrate settings for CAKE or set OLD_${setting} vars for HFSC
for qdisc in cake hfsc; do
eval "settings=\"\${${qdisc}_old_settings}\""
for setting in $settings; do
config_get val "$qdisc" "$setting"
[ -n "$val" ] || continue
has_old_settings=1
if [ "$qdisc" = "$root_qdisc" ]; then
eval "OLD_${setting}=\"${val}\""
# Immediately migrate CAKE settings
[ "$qdisc" = cake ] && { uci_tmp set "qosmate.advanced.$setting=$val" || return 1; }
fi
done
done
# Skip migration if not needed
if [ -n "$adv_preset" ]; then
if [ "$has_old_settings" = 0 ]; then
[ -n "$commit_req" ] || return 0
commit_tmp_config
return $?
fi
delete_premigration_settings
commit_tmp_config
return $?
fi
# Clean up old settings
delete_premigration_settings
# Finalize CAKE migration
[ "$root_qdisc" = cake ] && {
if [ -n "$OLD_COMMON_LINK_PRESETS" ]; then
commit_tmp_config || return 1
report_migration cake "$OLD_COMMON_LINK_PRESETS" "$OLD_OVERHEAD"
else
uci_tmp set qosmate.advanced.COMMON_LINK_PRESETS=ethernet &&
commit_tmp_config || return 1
log_msg "Added missing link layer preset: ethernet."
fi
return 0
}
# Migrate settings for non-cake root qdiscs
# Use default "ethernet" if LINKTYPE not set
case "${OLD_LINKTYPE:-ethernet}" in
atm)
COMMON_LINK_PRESETS=atm
# ATM used the $OH variable - only set if different from default
[ -n "$OLD_OH" ] && [ "$OLD_OH" != "44" ] && {
OH="$OLD_OH"
uci_tmp set qosmate.advanced.OVERHEAD="$OLD_OH" || return 1
}
;;
docsis)
COMMON_LINK_PRESETS=docsis
;;
*)
COMMON_LINK_PRESETS=ethernet
;;
esac
uci_tmp set qosmate.advanced.COMMON_LINK_PRESETS="$COMMON_LINK_PRESETS" &&
commit_tmp_config || return 1
report_migration "$root_qdisc" "$COMMON_LINK_PRESETS" "$OH"
:
}
### Servce functions
# Check if qosmate is active
# Return codes:
# 0 - active
# 1 - inactive
# 250 - error
is_qosmate_active() {
[ -n "$1" ] || { error_out "is_qosmate_active: specify WAN device"; return 250; }
tc qdisc show dev "$1" 2>/dev/null | grep -qE "hfsc|cake|htb" && \
nft -t list table inet dscptag >/dev/null 2>&1
}
# Validate a single nftables file
# Args: $1=file_path, $2=file_type, $3=tmp_file
# Returns: 0 on success, 1 on failure
validate_nft_file() {
local file_path="$1" file_type="$2" tmp_file="$3"
echo "Validating $file_type ($file_path):" >> "$tmp_file"
if [ ! -s "$file_path" ]; then
echo " - File is empty or does not exist. Skipping." >> "$tmp_file"
return 0
fi
case "$file_type" in
"full table rules")
if nft --check --file "$file_path" >> "$tmp_file" 2>&1; then
echo " - Syntax check PASSED." >> "$tmp_file"
return 0
else
echo " - Syntax check FAILED." >> "$tmp_file"
return 1
fi
;;
"inline rules")
validate_inline_rules "$file_path" "$tmp_file"
;;
*)
echo " - ERROR: Unknown file type '$file_type'." >> "$tmp_file"
return 1
;;
esac
}
# Validate inline rules with context wrapping
# Args: $1=file_path, $2=tmp_file
validate_inline_rules() {
local file_path="$1" tmp_file="$2"
local check_file="/tmp/qosmate_inline_init_check.nft"
echo " - Checking for forbidden keywords (table, chain, hook, priority)..." >> "$tmp_file"
if grep -Eq '^[[:space:]]*(table|chain|type|hook|priority)[[:space:]]+' "$file_path"; then
echo " - Keyword check FAILED. Forbidden keyword found." >> "$tmp_file"
return 1
fi
printf '%s\n%s\n' " - Keyword check PASSED." \
" - Checking NFT syntax (within context)..." >> "$tmp_file"
# Create temporary file with context
{
printf '%s\n\t%s\n' "table inet __qosmate_validation_ctx {" \
"chain __dscptag_init_ctx {"
cat "$file_path"
printf '\n\t%s\n%s\n' "}" "}"
} > "$check_file"
if nft --check --file "$check_file" 2>>"$tmp_file"; then
echo " - Syntax check PASSED." >> "$tmp_file"
rm -f "$check_file"
return 0
else
echo " - Syntax check FAILED." >> "$tmp_file"
rm -f "$check_file"
return 1
fi
}
validate_custom_rules() {
local tmp_file="/tmp/qosmate_custom_rules_validation.txt"
local fail=0
# Clear previous validation results
true > "$tmp_file"
# Delete existing table before validation to avoid conflicts with existing sets/meters
echo "Deleting existing qosmate_custom table before validation..." >> "$tmp_file"
nft destroy table inet qosmate_custom
# Validate both rule types
if ! validate_nft_file "$QOSMATE_CUSTOM_RULES_FILE" "full table rules" "$tmp_file"; then
fail=$((fail + 1))
fi
if ! validate_nft_file "$QOSMATE_INLINE_RULES_FILE" "inline rules" "$tmp_file"; then
fail=$((fail + 1))
fi
# Report final result
if [ $fail -eq 0 ]; then
printf '\n%s\n' "Overall validation: PASSED" >> "$tmp_file"
return 0
else
printf '\n%s\n' "Overall validation: FAILED" >> "$tmp_file"
return 1
fi
}
# Detect package manager (opkg or apk)
detect_package_manager() {
local has_apk=0 has_opkg=0
check_util apk && has_apk=1
check_util opkg && has_opkg=1
if [ $has_apk -eq 1 ] && [ $has_opkg -eq 1 ]; then
echo "conflict"
elif [ $has_apk -eq 1 ]; then
echo "apk"
elif [ $has_opkg -eq 1 ]; then
echo "opkg"
else
echo "unknown"
fi
}
# Check for valid package manager
check_pkg_manager() {
case "$1" in
apk|opkg)
return 0
;;
conflict)
error_out "Multiple package managers detected (apk and opkg). Package management disabled."
return 1
;;
*)
error_out "No supported package manager found."
return 1
;;
esac
}
# Check if a package is installed
check_package() {
local pkg="$1"
case "$pkg_manager" in
"apk")
apk list -I "$pkg" 2>/dev/null | grep -q "^$pkg-[0-9]"
return $?
;;
"opkg")
opkg list-installed | grep -q "^$pkg "
return $?
;;
*)
return 1
;;
esac
}
install_packages() {
# Flag to indicate if opkg update is needed
local need_update=0
check_pkg_manager "$pkg_manager" || return 1
# Check if any packages are missing
for pkg in $REQUIRED_PACKAGES; do
if ! check_package "$pkg"; then
print_msg "$pkg is not installed."
need_update=1
break
fi
done
# Run update if at least one package is missing
if [ "$need_update" -eq 1 ]; then
print_msg "Updating package list..."
case "$pkg_manager" in
"apk")
apk update
;;
"opkg")
opkg update
;;
esac
# Install missing packages
for pkg in $REQUIRED_PACKAGES; do
if ! check_package "$pkg"; then
log_msg "Installing $pkg..."
case "$pkg_manager" in
"apk")
apk add "$pkg" || {
error_out "Failed to install $pkg."
return 1 # Abort if the installation fails
}
;;
"opkg")
opkg install "$pkg" || {
error_out "Failed to install $pkg."
return 1 # Abort if the installation fails
}
;;
esac
fi
done
fi
}
check_package_status() {
local missing_packages=""
for pkg in $REQUIRED_PACKAGES; do
if ! check_package "$pkg"; then
missing_packages="$missing_packages $pkg"
fi
done
if [ -n "$missing_packages" ]; then
error_out "Missing packages:$missing_packages"
return 1
fi
return 0
}
create_hotplug_script() {
[ -s "$QOSMATE_HOTPLUG_SCRIPT" ] && { log_msg "Hotplug script already exists."; return 0; }
log_msg "" "Creating the hotplug script."
cat > "$QOSMATE_HOTPLUG_SCRIPT" << 'EOF'
#!/bin/sh
[ -n "$DEVICE" ] || exit 0
if [ "$ACTION" = ifup ]; then
. /lib/functions.sh
config_load qosmate || {
logger -t qosmate -p user.err "Failed to load config."
exit 1
}
config_get qosmate_enabled global enabled
if [ "$qosmate_enabled" = "1" ]; then
logger -t qosmate "Reloading qosmate.sh due to $ACTION of $INTERFACE ($DEVICE)"
/etc/init.d/qosmate enable
/etc/init.d/qosmate restart
else
logger -t qosmate "qosmate is disabled in the configuration. Not executing the script."
fi
fi
EOF
}
manage_custom_rules_file() {
local action="$1"
case "$action" in
create)
# Ensure the directory exists
try_mkdir -p "${QOSMATE_D}"
# Create the files if they don't exist
[ ! -f "$QOSMATE_CUSTOM_RULES_FILE" ] && touch "$QOSMATE_CUSTOM_RULES_FILE"
[ ! -f "$QOSMATE_INLINE_RULES_FILE" ] && touch "$QOSMATE_INLINE_RULES_FILE"
;;
delete)
# Not used at the moment...
rm -f "$QOSMATE_CUSTOM_RULES_FILE"
rm -f "$QOSMATE_INLINE_RULES_FILE"
;;
esac
}
start_service() {
# handle first run after installation or update from older versions
if [ "$VERSION" = dev ]; then
log_msg "" "Completing the upgrade of the update mechanism..."
update -f || return 1
# shellcheck disable=SC1090
. "${QOSMATE_SERVICE_PATH}" # source updated init script
fi
# Check files and re-fetch them if any are missing
for component in $QOSMATE_COMPONENTS; do
check_files_integrity "$component"
rv=$?
case $rv in
0) ;; # integrity OK
1) return 1 ;;
2|3) # missing files
local fix_upd_channel='' fix_version='' force_version=''
case "$component" in
BACKEND) fix_version="$VERSION" fix_upd_channel="$UPD_CHANNEL" ;;
FRONTEND) get_component_spec fix_version fix_upd_channel FRONTEND local 2>/dev/null
esac
case "$fix_version" in
dev|''|"1.1.0"|"v1.1.0") ;;
*) force_version="-W $fix_version"
esac
: "${fix_upd_channel:=release}"
if ! update -U "$fix_upd_channel" $force_version -c "$component"; then
log_msg -warn "Failed to fix qosmate installation automatically."
[ $rv = 2 ] && {
log_msg "Either connect to the Internet and run '/etc/init.d/qosmate start' to have missing files automatically fetched," \
"or manually download and save them in designated paths."
return 1
}
# Do not fail on missing reg files
fi ;;
4) ;; # non-matching md5sums
esac
done
install_packages
create_hotplug_script
load_and_fix_config || return 1
# Enable the global option
uci set qosmate.global.enabled='1'
uci commit qosmate
export global_enabled=1
# Create custom rules file if it doesn't exist
manage_custom_rules_file create
nft destroy table inet qosmate_custom
nft -f "$QOSMATE_CUSTOM_RULES_FILE"
preserve_config_files # Add config files to sysupgrade.conf
try_mkdir -p "$QOSMATE_RUN_DIR"
# Save the current WAN interface to a temporary file
printf '%s\n' "$WAN" > /tmp/qosmate_wan
/bin/sh "$QOSMATE_MAIN_SCRIPT" || { error_out "Failed to start qosmate."; return 1; }
/etc/init.d/firewall reload &&
enabled || /etc/init.d/qosmate enable || return 1
# Start autorate service if enabled (separate procd-managed service)
if [ "$AUTORATE_ENABLED" = "1" ] && [ -f /etc/init.d/qosmate-autorate ]; then
log_msg "" "Starting autorate service..."
/etc/init.d/qosmate-autorate enable 2>/dev/null
/etc/init.d/qosmate-autorate start
fi
log_msg "Service started"
}
stop_service() {
# Read the old WAN interface from the temporary file
OLD_WAN=$(cat /tmp/qosmate_wan 2>/dev/null)
if [ -z "$OLD_WAN" ]; then
# If the temporary file doesn't exist, fall back to WAN from config
load_and_fix_config
OLD_WAN="$WAN"
fi
print_msg "Stopping service qosmate..."
# Stop autorate service (separate procd-managed service)
/etc/init.d/qosmate-autorate stop 2>/dev/null
# Only disable if not in shutdown and not in restart
if [ "$DISABLE_ON_STOP" != "0" ] && [ -z "$FROM_SHUTDOWN" ]; then
# Keep autorate enable-state in sync with qosmate disable-state.
/etc/init.d/qosmate-autorate disable 2>/dev/null
disable
uci set qosmate.global.enabled='0'
uci commit qosmate
fi
# Remove custom rules table
nft destroy table inet qosmate_custom
## Delete files
rm -f "$QOSMATE_HOTPLUG_SCRIPT"
rm -f /usr/share/nftables.d/ruleset-post/dscptag.nft
## Delete the old qdiscs and IFB associated with the old WAN interface
tc qdisc del dev "$OLD_WAN" root > /dev/null 2>&1
tc qdisc del dev ifb-"$OLD_WAN" root > /dev/null 2>&1
tc qdisc del dev "$OLD_WAN" ingress > /dev/null 2>&1
# Remove IFB interface
ip link del ifb-"$OLD_WAN" 2>/dev/null
nft destroy table inet dscptag
# Remove temporary/runtime files
rm -f /tmp/qosmate_wan
rm -rf "$QOSMATE_RUN_DIR"
print_msg "Reloading network service..."
/etc/init.d/network reload
/etc/init.d/firewall reload
log_msg "Service stopped"
}
shutdown() {
# Set variable for shutdown
FROM_SHUTDOWN=1
# Call stop_service
stop_service
}
status_service() {
load_and_fix_config -nofix
print_msg "==== qosmate Status ===="
# Check if autostart is enabled
if enabled; then
print_msg "qosmate autostart is enabled."
else
print_msg "qosmate autostart is not enabled."
fi
# Check if the service is enabled in UCI config
local config_enabled=true not_managing=
if [ "$global_enabled" != 1 ]; then
config_enabled=false
not_managing=", but qosmate is not managing it"
fi
print_msg "qosmate global:enabled is $config_enabled."
local dir dev qdisc qdiscs active_qdisc \
IFB="ifb-$WAN"
# Check and report if traffic shaping is active
for dir in egress ingress; do
case "$dir" in
egress) dev="$WAN" ;;
ingress) dev="$IFB" ;;
esac
qdiscs="$(tc qdisc show dev "$dev" 2>/dev/null | sed -n 's/^qdisc\s\s*\([^ \t]*\).* root .*/\1/p')"
qdiscs="${qdiscs//$'\n'/ }"
active_qdisc=''
for qdisc in cake cake_mq hfsc htb; do
case "${qdiscs}" in "${qdisc}"|"${qdisc} "*|*" ${qdisc}"|*" ${qdisc} "*)
active_qdisc="$qdisc"
esac
done
case "$active_qdisc" in
cake|cake_mq) qdisc_print=CAKE ;;
hfsc) qdisc_print=HFSC ;;
htb) qdisc_print=HTB ;;
esac
if [ -n "$active_qdisc" ]; then
print_msg "Traffic shaping ($qdisc_print) is active on the $dir interface ($dev)$not_managing."
else
print_msg "No traffic shaping is active on the $dir interface ($dev)."
fi
done
# Show summary of current settings
print_msg "" "==== Current Settings ====" \
"Upload rate: $UPRATE kbps" \
"Download rate: $DOWNRATE kbps" \
"Game traffic upload: $GAMEUP kbps" \
"Game traffic download: $GAMEDOWN kbps"
if [ "$ROOT_QDISC" = "cake" ]; then
print_msg "Queue discipline: CAKE (Root qdisc)"
local active_cake_type=''
read -r active_cake_type < /tmp/qosmate/cake_type 2>/dev/null
if [ "$active_cake_type" = "cake_mq" ]; then
print_msg "Multi-Queue CAKE: active (cake_mq)"
elif [ "$USE_MQ" = "1" ]; then
if [ "$active_cake_type" = "cake" ]; then
print_msg "Multi-Queue CAKE: not available, using standard cake"
else
print_msg "Multi-Queue CAKE: enabled in config (service not running)"
fi
else
print_msg "Multi-Queue CAKE: off"
fi
elif [ "$ROOT_QDISC" = "htb" ]; then
print_msg "Queue discipline: HTB (Root qdisc)"
else
print_msg "Queue discipline: $gameqdisc (for game traffic in HFSC)"
if [ "$ROOT_QDISC" = "hybrid" ] && [ "$USE_MQ" = "1" ]; then
print_msg "Multi-Queue CAKE: not supported in hybrid mode"
fi
fi
if [ "$AUTORATE_ENABLED" = "1" ]; then
if [ -f /etc/init.d/qosmate-autorate ] && /etc/init.d/qosmate-autorate running; then
print_msg "Autorate: active"
else
print_msg "Autorate: enabled in config but not running"
fi
else
print_msg "Autorate: off"
fi
# Display QoSmate version information
print_msg "" "==== Version Information ===="
check_version
# Display system information
print_msg "" "==== System Information ===="
ubus call system board
# Display health check information
print_msg "" "==== Health Check ===="
health_check
# Check for flow offloading settings (incompatible with qosmate)
print_msg "" "==== Flow Offloading Check ===="
local flow_offloading_enabled=0
local flow_offloading_hw_enabled=0
# Check software flow offloading in firewall defaults
if [ "$(uci -q get firewall.@defaults[0].flow_offloading)" = "1" ]; then
flow_offloading_enabled=1
print_msg "WARNING: Software flow offloading is enabled."
fi
# Check hardware flow offloading in firewall defaults
if [ "$(uci -q get firewall.@defaults[0].flow_offloading_hw)" = "1" ]; then
flow_offloading_hw_enabled=1
print_msg "WARNING: Hardware flow offloading is enabled."
fi
if [ $flow_offloading_enabled -eq 1 ] || [ $flow_offloading_hw_enabled -eq 1 ]; then
print_msg "CRITICAL: Flow offloading is incompatible with qosmate and may cause issues!"
print_msg "To disable flow offloading, run:"
if [ $flow_offloading_enabled -eq 1 ]; then
print_msg " uci set firewall.@defaults[0].flow_offloading='0'"
fi
if [ $flow_offloading_hw_enabled -eq 1 ]; then
print_msg " uci set firewall.@defaults[0].flow_offloading_hw='0'"
fi
print_msg " uci commit firewall"
print_msg " /etc/init.d/firewall restart"
else
print_msg "Flow offloading is disabled (compatible with qosmate)."
fi
# Display WAN interface information
print_msg "" "==== WAN Interface Information ===="
ifstatus wan | grep -e device
# Display QoSmate configuration
print_msg "" "==== QoSmate Configuration ===="
cat "$QOSMATE_CFG_FILE"
print_msg "==== Package Status ===="
if check_pkg_manager "$pkg_manager" && check_package_status; then
print_msg "All required packages are installed."
else
print_msg "Some required packages are missing. QoSmate may not function correctly."
fi
print_msg "" "==== Detailed Technical Information ====" \
"Traffic Control (tc) Queues:"
tc -s qdisc
print_msg "" "==== Nftables Ruleset (dscptag) ===="
nft list ruleset | grep 'chain dscptag' -A 100
print_msg "" "==== Custom Rules Table Status ===="
if nft list table inet qosmate_custom &>/dev/null; then
print_msg "Custom rules table (qosmate_custom) is active." \
"Current custom rules:"
nft list table inet qosmate_custom
else
print_msg "Custom rules table (qosmate_custom) is not active or doesn't exist."
fi
print_msg "" "==== Inline Rules Status ===="
if [ -s "$QOSMATE_INLINE_RULES_FILE" ]; then
print_msg "Inline rules are configured:"
cat "$QOSMATE_INLINE_RULES_FILE"
printf '\n'
else
print_msg "No inline rules configured."
fi
}
restart() {
DISABLE_ON_STOP=0 stop_service
sleep 1 # Ensure all processes have been properly terminated
start_service
}
reload_service() {
restart
}
# 1 - speedtest command
# 2 - Upload|Download
# I/O via STDIN/STDOUT
parse_speedtest_output() {
local speedtest_cmd="$1" direction="$2"
case "$speedtest_cmd" in
speedtest-go*)
grep "${direction}:" | grep -oE '[0-9]+\.[0-9]+' | head -n1
;;
"speedtest --simple"*)
# match to (Upload:|Download:).*, print field 2. if no match, return 1
awk "BEGIN{rv=1} \$0~/$direction:/ {print \$2; rv=0; exit} END{exit rv}"
;;
*)
error_out "Unexpected speedtest command '$speedtest_cmd'."
false
;;
esac || {
error_out "Failed to get the $direction speed."
echo "0"
return 1
}
:
}
auto_setup() {
try_auto_setup "$@" && return 0
error_out "auto-setup failed."
uci revert qosmate 2>/dev/null
return 1
}
# for non-interactive setup, call with '-n [gaming_ip_address]'
try_auto_setup() {
# shellcheck disable=SC2329
is_ip_configured() {
local ip ip_type section_id="$1" gaming_ip="$2"
for ip_type in src_ip dest_ip; do
config_get ip "$section_id" "$ip_type"
[ "$ip" = "$gaming_ip" ] && { ip_configured=1; break; }
done
}
local L3_DEVICE WAN_INTERFACE FINAL_INTERFACE SPEEDTEST_CMD FREE_SPACE SPEED_RESULT DOWNLOAD_SPEED UPLOAD_SPEED
local gaming_ip='' noninteractive='' speed_choice response speedtest_req direction
if [ "$1" = "-n" ]; then
gaming_ip="$2"
noninteractive=1
else
noninteractive=
fi
[ -z "$noninteractive" ] && print_msg "Starting qosmate auto-setup..."
# Detect WAN interface
WAN_INTERFACE=$(ifstatus wan | grep -e '"device"' | cut -d'"' -f4)
L3_DEVICE=$(ifstatus wan | grep -e '"l3_device"' | cut -d'"' -f4)
if [ -z "$WAN_INTERFACE" ] && [ -z "$L3_DEVICE" ]; then
error_out "Unable to detect WAN interface. Please set it manually in the configuration."
return 1
fi
FINAL_INTERFACE=${L3_DEVICE:-$WAN_INTERFACE}
print_msg "Detected WAN interface: $FINAL_INTERFACE"
# Stop qosmate if it's running
if is_qosmate_active "$FINAL_INTERFACE"; then
print_msg "Stopping qosmate for accurate speed test results..."
stop_service
sleep 5 # Give some time for the network to stabilize
fi
if [ -n "$noninteractive" ]; then
speedtest_req=1
else
while :; do
print_msg "Do you want to run a speed test or enter speeds manually? [test/manual]"
read -r speed_choice
case "$speed_choice" in
*[A-Z]*) speed_choice="$(printf %s "$speed_choice" | tr 'A-Z' 'a-z')"
esac
case "$speed_choice" in
test|manual) break
esac
print_msg "Invalid input '$speed_choice'. Please enter 'test' or 'manual'."
done
if [[ "$speed_choice" = manual ]]; then
for direction in download upload; do
while :; do
print_msg "Please enter your $direction speed in Mbit/s:"
read -r response
case "$response" in
''|*[!0-9.]*|*.*.*)
# do not allow empty string or irrelevant characters or 2x '.'
print_msg "Invalid input '$response'. Please try again."
continue
;;
*[0-9]*)
break
;;
*)
# do not allow input without digits
print_msg "Invalid input '$response'. Please try again."
continue
;;
esac
done
case "$direction" in
download) DOWNLOAD_SPEED="$response" ;;
upload) UPLOAD_SPEED="$response" ;;
esac
done
speedtest_req=
else
print_msg "This will run a speed test to configure qosmate. Do you want to continue? [y/N]"
read -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
print_msg "Auto-setup cancelled."
return 0
fi
speedtest_req=1
fi
fi
if [ -n "$speedtest_req" ]; then
# Check for speedtest-go first
if check_util speedtest-go; then
print_msg "speedtest-go is already installed. Using it for the speed test."
SPEEDTEST_CMD="speedtest-go"
else
print_msg "speedtest-go is not found. Checking for python3-speedtest-cli..."
if check_util speedtest; then
print_msg "python3-speedtest-cli is already installed. Using it for the speed test."
SPEEDTEST_CMD="speedtest --simple"
else
print_msg "Neither speedtest-go nor python3-speedtest-cli is installed. Attempting to install speedtest-go..."
check_pkg_manager "$pkg_manager" || return 1
# Check for sufficient space (adjust the required space as needed)
FREE_SPACE=$(df /overlay | awk 'NR==2 {print $4}')
if [ "$FREE_SPACE" -lt 15360 ]; then # Assuming 15MB for speedtest-go
print_msg "Not enough space for speedtest-go. Attempting to install python3-speedtest-cli instead..."
else
case "$pkg_manager" in
"apk")
apk update && apk add speedtest-go
;;
"opkg")
opkg update && opkg install speedtest-go
;;
esac
if [ $? -eq 0 ]; then
SPEEDTEST_CMD="speedtest-go"
else
error_out "Failed to install speedtest-go. Attempting to install python3-speedtest-cli instead..."
fi
fi
# If speedtest-go installation failed or there wasn't enough space, try python3-speedtest-cli
if [ -z "$SPEEDTEST_CMD" ]; then
if [ "$FREE_SPACE" -lt 1024 ]; then # 1MB for python3-speedtest-cli
error_out "Error: Not enough free space to install any speedtest tool." \
"Auto-setup cannot continue. Please free up some space and try again."
return 1
fi
case "$pkg_manager" in
"apk")
apk update && apk add python3-speedtest-cli
;;
"opkg")
opkg update && opkg install python3-speedtest-cli python3-speedtest-cli-src
;;
esac
if [ $? -eq 0 ]; then
SPEEDTEST_CMD="speedtest --simple"
else
error_out "Failed to install python3-speedtest-cli. Auto-setup cannot continue."
return 1
fi
fi
fi
fi
print_msg "Running speed test... This may take a few minutes."
SPEED_RESULT=$($SPEEDTEST_CMD)
DOWNLOAD_SPEED=$(printf %s "$SPEED_RESULT" | parse_speedtest_output "$SPEEDTEST_CMD" Download) &&
UPLOAD_SPEED=$(printf %s "$SPEED_RESULT" | parse_speedtest_output "$SPEEDTEST_CMD" Upload) ||
return 1
print_msg "Speed test results:" \
"Download speed: $DOWNLOAD_SPEED Mbit/s" \
"Upload speed: $UPLOAD_SPEED Mbit/s"
fi
# Convert speeds to kbps and apply 90% rule
DOWNRATE=$(awk -v speed="$DOWNLOAD_SPEED" 'BEGIN {print int(speed * 1000 * 0.9)}')
UPRATE=$(awk -v speed="$UPLOAD_SPEED" 'BEGIN {print int(speed * 1000 * 0.9)}')
print_msg "QoS configuration:" \
"DOWNRATE: $DOWNRATE kbps (90% of measured download speed)" \
"UPRATE: $UPRATE kbps (90% of measured upload speed)"
[ -f "$QOSMATE_CFG_FILE" ] || {
error_out "config file '$QOSMATE_CFG_FILE' not found."
return 1
}
config_load qosmate &&
uci set qosmate.settings.WAN="$FINAL_INTERFACE" &&
uci set qosmate.settings.DOWNRATE="$DOWNRATE" &&
uci set qosmate.settings.UPRATE="$UPRATE" || return 1
# Section for gaming device IP
if [ -z "$noninteractive" ]; then
print_msg "Would you like to add a gaming device IP for prioritization? [y/N]"
read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
print_msg "Please enter the IP address of your gaming device:"
read -r gaming_ip
fi
fi
if [ -n "$gaming_ip" ]; then
# Validate IP address format
if [[ $gaming_ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
# Check if rules for this IP already exist
local ip_configured=
config_foreach is_ip_configured rule "$gaming_ip"
# $ip_configured is set in is_ip_configured()
if [ -n "$ip_configured" ]; then
print_msg "Rules for IP $gaming_ip already exist. Skipping addition of new rules."
gaming_ip=
fi
else
print_msg "Invalid IP address format. No gaming device rules added."
gaming_ip=
fi
fi
if [ -n "$gaming_ip" ]; then
local direction ip_dir port_dir
for direction in Inbound Outbound; do
case "$direction" in
Inbound) ip_dir=dest port_dir=src ;;
Outbound) ip_dir=src port_dir=dest ;;
esac
uci add qosmate rule 1>/dev/null &&
uci set "qosmate.@rule[-1].name=Game_Console_${direction}" &&
uci set "qosmate.@rule[-1].proto=udp" &&
uci set "qosmate.@rule[-1].${ip_dir}_ip=${gaming_ip}" &&
uci add_list "qosmate.@rule[-1].${port_dir}_port=!=80" &&
uci add_list "qosmate.@rule[-1].${port_dir}_port=!=443" &&
uci set "qosmate.@rule[-1].class=cs5" &&
uci set "qosmate.@rule[-1].counter=1" ||
return 1
done
print_msg "Gaming device rules added for IP: $gaming_ip"
else
print_msg "No gaming device IP added."
fi
uci commit qosmate || return 1
print_msg "Configuration updated. New settings:"
grep -E "option (WAN|DOWNRATE|UPRATE)" "$QOSMATE_CFG_FILE"
print_msg "Auto-setup complete. qosmate has been configured with detected settings." \
"To apply these changes, please restart qosmate by running: $QOSMATE_SERVICE_PATH restart"
:
}
auto_setup_noninteractive() {
local output_file="/tmp/qosmate_auto_setup_output.txt" auto_setup_rv
{
echo "Starting qosmate non-interactive auto-setup..."
auto_setup -n "$1" 2>&1
} > "$output_file"
auto_setup_rv=$?
echo "$output_file"
return $auto_setup_rv
}
health_check() {
# Check if QoSmate is properly configured and running
local status="" errors=0 service_enabled=0 config_status=''
# Check the config
if load_and_fix_config -nofix 1>/dev/null; then
config_status=ok
else
config_status=failed # *** NOTE THE CHANGE FROM 'missing' TO 'failed' ***
errors=$((errors + 1))
fi
# Check global:enabled and if the service is enabled
enabled && service_enabled=1
case "${service_enabled}${global_enabled}" in
11) status="${status}service:enabled;" ;;
00) status="${status}service:disabled;"; errors=$((errors + 1)) ;;
*) status="${status}service:invalid_state;"; errors=$((errors + 1)) ;;
esac
# Check nftables configuration
if nft list table inet dscptag >/dev/null 2>&1; then
status="${status}nft:ok;"
else
status="${status}nft:failed;"
errors=$((errors + 1))
fi
# Check tc configuration on WAN interface
if [ -n "$WAN" ] && tc qdisc show dev "$WAN" 2>/dev/null | grep -E -q "hfsc|cake|htb"; then
status="${status}tc:ok;"
else
status="${status}tc:failed;"
errors=$((errors + 1))
fi
status="${status}config:${config_status};"
# Check required packages
local missing_packages=""
for pkg in $REQUIRED_PACKAGES; do
if ! check_package "$pkg"; then
missing_packages="${missing_packages}${pkg} "
fi
done
if [ -z "$missing_packages" ]; then
status="${status}packages:ok;"
else
status="${status}packages:missing:${missing_packages};"
errors=$((errors + 1))
fi
# Check files existence and md5sums
for component in ${QOSMATE_COMPONENTS}; do
if check_files_integrity "$component"; then
status="${status}${component}_integrity:ok;"
else
status="${status}${component}_integrity:failed;"
errors=$((errors + 1))
fi
done
# Output status
echo "status=$status;errors=$errors"
# Return code depending on errors
[ $errors -eq 0 ] || return 1
return 0
}
pkg_manager="$(detect_package_manager)" # global variable
### Process command-line args
# if called directrly via /bin/sh with one of the keywords, set $action to the keyword
case "$1" in
update|print_file_list|post_update_1|post_update_2) action="$1"; shift
esac
case "$action" in
update|print_file_list|post_update_1|post_update_2) "$action" "$@"; exit $? ;;
esac
:
service_triggers() {
procd_add_reload_trigger "qosmate"
}