mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-08-02 07:29:28 +08:00
2574 lines
90 KiB
Bash
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"
|
|
}
|