Files
NeoTachyon/podkop/files/usr/lib/updater.sh
T

1578 lines
52 KiB
Bash

# shellcheck shell=ash
UPDATES_TMP_DIR=""
UPDATES_TARGET_ARCH=""
UPDATES_ARCH_CANDIDATES=""
UPDATES_ZAPRET_ARCH=""
UPDATES_ZAPRET_BUNDLE_URL=""
UPDATES_ZAPRET_BUNDLE_NAME=""
UPDATES_ZAPRET_PACKAGE_FILE=""
UPDATES_ZAPRET_PACKAGE_NAME=""
UPDATES_ZAPRET_PACKAGE_VERSION=""
UPDATES_BYEDPI_ARCH=""
UPDATES_BYEDPI_PACKAGE_URL=""
UPDATES_BYEDPI_PACKAGE_NAME=""
UPDATES_BYEDPI_PACKAGE_FILE=""
UPDATES_BYEDPI_PACKAGE_VERSION=""
UPDATES_SING_BOX_EXTENDED_RELEASE_TAG=""
UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX=""
UPDATES_SING_BOX_EXTENDED_ASSET_URL=""
UPDATES_SING_BOX_EXTENDED_ASSET_NAME=""
UPDATES_JOB_DIR="/var/run/podkop-plus/component-actions"
UPDATES_LOCK_DIR="/var/run/podkop-plus/component-action.lock"
UPDATES_LOCK_HELD=0
UPDATES_PODKOP_WAS_RUNNING=0
updates_log() {
local message="$1"
local level="${2:-info}"
log "Updates: $message" "$level"
}
updates_init_tmp_dir() {
[ -n "$UPDATES_TMP_DIR" ] && return 0
UPDATES_TMP_DIR="$(mktemp -d /tmp/podkop-plus-updates.XXXXXX 2>/dev/null || true)"
if [ -z "$UPDATES_TMP_DIR" ]; then
UPDATES_TMP_DIR="/tmp/podkop-plus-updates.$$"
mkdir -p "$UPDATES_TMP_DIR" || return 1
fi
}
updates_cleanup() {
[ -n "$UPDATES_TMP_DIR" ] && rm -rf "$UPDATES_TMP_DIR"
}
updates_acquire_component_lock() {
local owner_pid
mkdir -p /var/run/podkop-plus || return 1
if mkdir "$UPDATES_LOCK_DIR" 2>/dev/null; then
printf '%s\n' "$$" >"$UPDATES_LOCK_DIR/pid"
UPDATES_LOCK_HELD=1
return 0
fi
owner_pid="$(sed -n '1p' "$UPDATES_LOCK_DIR/pid" 2>/dev/null)"
if [ -n "$owner_pid" ] && kill -0 "$owner_pid" 2>/dev/null; then
return 1
fi
rm -f "$UPDATES_LOCK_DIR/pid" 2>/dev/null
rmdir "$UPDATES_LOCK_DIR" 2>/dev/null || return 1
mkdir "$UPDATES_LOCK_DIR" 2>/dev/null || return 1
printf '%s\n' "$$" >"$UPDATES_LOCK_DIR/pid"
UPDATES_LOCK_HELD=1
}
updates_release_component_lock() {
[ "$UPDATES_LOCK_HELD" -eq 1 ] || return 0
rm -f "$UPDATES_LOCK_DIR/pid" 2>/dev/null
rmdir "$UPDATES_LOCK_DIR" 2>/dev/null
UPDATES_LOCK_HELD=0
}
updates_component_action_cleanup() {
updates_cleanup
updates_release_component_lock
}
updates_json_response() {
local success="$1"
local component="$2"
local action="$3"
local message="$4"
local current_version="${5:-}"
local latest_version="${6:-}"
local changed="${7:-0}"
local status="${8:-}"
jq -cn \
--argjson success "$success" \
--arg component "$component" \
--arg action "$action" \
--arg message "$message" \
--arg current_version "$current_version" \
--arg latest_version "$latest_version" \
--argjson changed "$changed" \
--arg status "$status" \
'{
success: $success,
component: $component,
action: $action,
message: $message,
current_version: $current_version,
latest_version: $latest_version,
changed: $changed,
status: $status
}'
}
updates_success() {
updates_json_response true "$@"
exit 0
}
updates_fail() {
local component="$1"
local action="$2"
local message="$3"
local current_version="${4:-}"
local latest_version="${5:-}"
updates_log "$message" "error"
updates_json_response false "$component" "$action" "$message" "$current_version" "$latest_version" 0
exit 1
}
updates_job_json_response() {
local success="$1"
local job_id="$2"
local message="${3:-}"
jq -cn \
--argjson success "$success" \
--arg job_id "$job_id" \
--arg message "$message" \
'{
success: $success,
job_id: $job_id,
message: $message
}'
}
updates_job_state_path() {
local job_id="$1"
case "$job_id" in
*[!A-Za-z0-9._-]* | "" | "." | "..")
return 1
;;
esac
printf '%s/%s.json\n' "$UPDATES_JOB_DIR" "$job_id"
}
updates_write_running_job_state() {
local state_file="$1"
local component="$2"
local action="$3"
mkdir -p "$UPDATES_JOB_DIR" || return 1
jq -cn \
--argjson success true \
--argjson running true \
--arg component "$component" \
--arg action "$action" \
--arg message "Component action is running" \
'{
success: $success,
running: $running,
component: $component,
action: $action,
message: $message,
current_version: "",
latest_version: "",
changed: 0,
status: "",
exit_code: null
}' >"$state_file"
}
updates_write_finished_job_state() {
local state_file="$1"
local component="$2"
local action="$3"
local exit_code="$4"
local output_file="$5"
local tmp_file json_file raw_output
tmp_file="${state_file}.$$"
json_file="$output_file.json"
if jq -e . "$output_file" >/dev/null 2>&1; then
jq -c \
--argjson running false \
--argjson exit_code "$exit_code" \
'. + {running: $running, exit_code: $exit_code}' \
"$output_file" >"$tmp_file" && mv "$tmp_file" "$state_file"
rm -f "$tmp_file" "$output_file"
return 0
fi
sed -n 's/^[^{]*\({.*\)$/\1/p' "$output_file" 2>/dev/null | tail -n 1 >"$json_file"
if [ -s "$json_file" ] && jq -e . "$json_file" >/dev/null 2>&1; then
jq -c \
--argjson running false \
--argjson exit_code "$exit_code" \
'. + {running: $running, exit_code: $exit_code}' \
"$json_file" >"$tmp_file" && mv "$tmp_file" "$state_file"
rm -f "$tmp_file" "$json_file" "$output_file"
return 0
fi
rm -f "$json_file"
raw_output="$(tr '\n' ' ' <"$output_file" 2>/dev/null | cut -c1-240)"
[ -n "$raw_output" ] || raw_output="Failed to execute"
jq -cn \
--argjson success false \
--argjson running false \
--arg component "$component" \
--arg action "$action" \
--arg message "$raw_output" \
--argjson exit_code "$exit_code" \
'{
success: $success,
running: $running,
component: $component,
action: $action,
message: $message,
current_version: "",
latest_version: "",
changed: 0,
status: "",
exit_code: $exit_code
}' >"$tmp_file" && mv "$tmp_file" "$state_file"
rm -f "$tmp_file" "$output_file"
}
component_action_async() {
local component="$1"
local action="$2"
local job_id state_file output_file
mkdir -p "$UPDATES_JOB_DIR" || {
updates_job_json_response false "" "Failed to create component action state directory"
exit 1
}
find "$UPDATES_JOB_DIR" -type f \( -name '*.json' -o -name '*.out' \) -mmin +60 -delete 2>/dev/null || true
job_id="$(date +%s 2>/dev/null)-$$"
state_file="$(updates_job_state_path "$job_id")" || {
updates_job_json_response false "" "Failed to prepare component action job"
exit 1
}
output_file="$UPDATES_JOB_DIR/$job_id.out"
updates_write_running_job_state "$state_file" "$component" "$action" || {
updates_job_json_response false "" "Failed to write component action state"
exit 1
}
(
trap '' HUP
/usr/bin/podkop-plus component_action "$component" "$action" >"$output_file" 2>&1
updates_write_finished_job_state "$state_file" "$component" "$action" "$?" "$output_file"
) >/dev/null 2>&1 &
updates_job_json_response true "$job_id" "Component action started"
}
component_action_status() {
local job_id="$1"
local state_file
state_file="$(updates_job_state_path "$job_id")" || {
updates_json_response false "unknown" "status" "Invalid component action job id" "" "" 0 ""
exit 1
}
if [ ! -f "$state_file" ]; then
updates_json_response false "unknown" "status" "Component action job was not found" "" "" 0 ""
exit 1
fi
cat "$state_file"
}
updates_command_exists() {
command -v "$1" >/dev/null 2>&1
}
updates_is_apk() {
updates_command_exists apk
}
updates_read_openwrt_release_value() {
local key="$1"
[ -f /etc/openwrt_release ] || return 0
sed -n "s/^${key}='\(.*\)'/\1/p" /etc/openwrt_release 2>/dev/null | head -n 1
}
updates_http_get() {
local url="$1"
if updates_command_exists curl; then
curl --connect-timeout 5 -m 30 -fsSL "$url"
return $?
fi
if updates_command_exists wget; then
wget -T 30 -qO- "$url"
return $?
fi
return 1
}
updates_download_file_once() {
local url="$1"
local output_path="$2"
if updates_command_exists curl; then
curl --connect-timeout 5 -m 120 -fsSL "$url" -o "$output_path"
return $?
fi
if updates_command_exists wget; then
wget -T 120 -q -O "$output_path" "$url"
return $?
fi
return 1
}
updates_download_with_retry() {
local url="$1"
local output_path="$2"
local label="$3"
local attempt=1
local max_attempts=3
while [ "$attempt" -le "$max_attempts" ]; do
updates_log "Downloading $label ($attempt/$max_attempts)"
if updates_download_file_once "$url" "$output_path" && [ -s "$output_path" ]; then
return 0
fi
rm -f "$output_path"
updates_log "Retrying $label" "warn"
attempt=$((attempt + 1))
done
return 1
}
updates_log_command() {
local description="$1"
local status output_file line level
shift
output_file="$(mktemp /tmp/podkop-plus-updates-command.XXXXXX 2>/dev/null || true)"
[ -n "$output_file" ] || output_file="/tmp/podkop-plus-updates-command.$$"
updates_log "$description"
"$@" >"$output_file" 2>&1
status=$?
level="info"
[ "$status" -eq 0 ] || level="error"
while IFS= read -r line; do
[ -n "$line" ] && updates_log "$line" "$level"
done <"$output_file"
rm -f "$output_file"
return "$status"
}
updates_pkg_is_installed() {
local package_name="$1"
if updates_is_apk; then
apk info -e "$package_name" >/dev/null 2>&1
return $?
fi
opkg list-installed 2>/dev/null | grep -Eq "^${package_name}([[:space:]-]|$)"
}
updates_get_installed_package_version() {
local package_name="$1"
if updates_is_apk; then
apk info -e "$package_name" >/dev/null 2>&1 || return 0
apk info -v "$package_name" 2>/dev/null | sed "s/^${package_name}-//" | sed -n '1p'
return 0
fi
opkg list-installed 2>/dev/null | awk -v pkg="$package_name" '$1 == pkg && $2 == "-" {print $3; exit}'
}
updates_get_available_package_version() {
local package_name="$1"
if updates_is_apk; then
apk policy "$package_name" 2>/dev/null |
awk '
/^ [^[:space:]][^[:space:]]*:/ {
version=$1
sub(/:$/, "", version)
print version
exit
}
'
return 0
fi
opkg list "$package_name" 2>/dev/null |
awk -v pkg="$package_name" '$1 == pkg && $2 == "-" {print $3; exit}'
}
updates_pkg_list_update() {
if updates_is_apk; then
apk update </dev/null
else
opkg update </dev/null
fi
}
updates_pkg_install_name() {
local package_name="$1"
if updates_is_apk; then
apk add "$package_name" </dev/null
else
opkg install "$package_name" </dev/null
fi
}
updates_pkg_install_name_downgrade() {
local package_name="$1"
if updates_is_apk; then
apk add "$package_name" </dev/null
else
opkg install --force-reinstall --force-downgrade "$package_name" </dev/null ||
opkg install --force-downgrade "$package_name" </dev/null
fi
}
updates_pkg_install_files() {
if updates_is_apk; then
apk add --allow-untrusted "$@" </dev/null
else
opkg install --force-overwrite --force-downgrade "$@" </dev/null
fi
}
updates_pkg_remove_name() {
local package_name="$1"
if ! updates_pkg_is_installed "$package_name"; then
return 0
fi
if updates_is_apk; then
apk del "$package_name" </dev/null
else
opkg remove --force-depends "$package_name" </dev/null
fi
}
updates_package_version_newer() {
local candidate_version="$1"
local installed_version="$2"
local apk_compare_result newest
[ -n "$candidate_version" ] || return 1
[ -n "$installed_version" ] || return 1
[ "$candidate_version" != "$installed_version" ] || return 1
if updates_is_apk; then
apk_compare_result="$(apk version -t "$candidate_version" "$installed_version" 2>/dev/null || true)"
[ "$apk_compare_result" = ">" ]
return $?
fi
if updates_command_exists opkg; then
opkg compare-versions "$candidate_version" ">" "$installed_version" >/dev/null 2>&1
return $?
fi
newest="$(printf '%s\n%s\n' "$installed_version" "$candidate_version" | sort -V | tail -n 1)"
[ "$newest" = "$candidate_version" ]
}
updates_compare_versions() {
local lhs="$1"
local rhs="$2"
local newest
[ -n "$lhs" ] || return 1
[ -n "$rhs" ] || return 1
[ "$lhs" = "$rhs" ] && echo 0 && return 0
if updates_is_apk; then
case "$(apk version -t "$lhs" "$rhs" 2>/dev/null || true)" in
">") echo 1 && return 0 ;;
"<") echo -1 && return 0 ;;
"=") echo 0 && return 0 ;;
esac
fi
if updates_command_exists opkg; then
if opkg compare-versions "$lhs" ">" "$rhs" >/dev/null 2>&1; then
echo 1
return 0
fi
if opkg compare-versions "$lhs" "<" "$rhs" >/dev/null 2>&1; then
echo -1
return 0
fi
if opkg compare-versions "$lhs" "=" "$rhs" >/dev/null 2>&1; then
echo 0
return 0
fi
fi
newest="$(printf '%s\n%s\n' "$rhs" "$lhs" | sort -V | tail -n 1)"
if [ "$newest" = "$lhs" ]; then
echo 1
else
echo -1
fi
}
updates_status_from_compare() {
local compare_result="$1"
case "$compare_result" in
-1) printf '%s\n' "outdated" ;;
0) printf '%s\n' "latest" ;;
1) printf '%s\n' "dev" ;;
*) return 1 ;;
esac
}
updates_check_success() {
local component="$1"
local current_version="$2"
local latest_version="$3"
updates_check_success_compared "$component" "$current_version" "$latest_version" "$current_version" "$latest_version"
}
updates_check_success_compared() {
local component="$1"
local current_version="$2"
local latest_version="$3"
local compare_current_version="$4"
local compare_latest_version="$5"
local compare_result status message
compare_result="$(updates_compare_versions "$compare_current_version" "$compare_latest_version" 2>/dev/null || true)"
[ -n "$compare_result" ] || updates_fail "$component" "check_update" "Failed to compare versions" "$current_version" "$latest_version"
status="$(updates_status_from_compare "$compare_result")" || updates_fail "$component" "check_update" "Failed to compare versions" "$current_version" "$latest_version"
case "$status" in
latest)
message="Latest version is installed"
updates_log "$component is up to date ($current_version)"
;;
outdated)
message="Update is available"
updates_log "$component update is available: $current_version -> $latest_version"
;;
dev)
message="Installed version is newer than release"
updates_log "$component installed version is newer than upstream release: $current_version -> $latest_version"
;;
esac
updates_success "$component" "check_update" "$message" "$current_version" "$latest_version" 0 "$status"
}
updates_ensure_package_tool() {
local tool_name="$1"
local package_name="$2"
if updates_command_exists "$tool_name"; then
return 0
fi
updates_log_command "Updating package lists before installing $package_name" updates_pkg_list_update || return 1
updates_log_command "Installing bootstrap package $package_name" updates_pkg_install_name "$package_name"
}
updates_retry_resolve() {
local description="$1"
local command_name="$2"
local attempt=1
local max_attempts=3
while [ "$attempt" -le "$max_attempts" ]; do
if "$command_name"; then
return 0
fi
updates_log "$description failed ($attempt/$max_attempts)" "warn"
attempt=$((attempt + 1))
sleep 2
done
return 1
}
updates_clear_version_caches() {
rm -f /tmp/podkop-plus.latest-version.cache
rm -f "$PODKOP_SYSTEM_INFO_CACHE_FILE"
rm -f /tmp/podkop-plus/system-info.json
}
updates_capture_podkop_running_state() {
UPDATES_PODKOP_WAS_RUNNING=0
[ -x "$PODKOP_SERVICE_INIT" ] || return 0
if "$PODKOP_SERVICE_INIT" status >/dev/null 2>&1; then
UPDATES_PODKOP_WAS_RUNNING=1
fi
}
updates_restart_podkop_after_successful_change() {
[ -x "$PODKOP_SERVICE_INIT" ] || return 0
if [ "$UPDATES_PODKOP_WAS_RUNNING" != "1" ]; then
updates_log "Podkop Plus was not running before component change; restart skipped"
return 0
fi
updates_log_command "Restarting Podkop Plus after successful component change" "$PODKOP_SERVICE_INIT" restart || true
}
updates_append_arch_candidate() {
local candidate="$1"
[ -n "$candidate" ] || return 0
case "$candidate" in
all | noarch)
return 0
;;
esac
case " $UPDATES_ARCH_CANDIDATES " in
*" $candidate "*)
return 0
;;
esac
if [ -n "$UPDATES_ARCH_CANDIDATES" ]; then
UPDATES_ARCH_CANDIDATES="$UPDATES_ARCH_CANDIDATES $candidate"
else
UPDATES_ARCH_CANDIDATES="$candidate"
fi
}
updates_append_arch_candidate_variants() {
local candidate="$1"
local base_candidate suffix
[ -n "$candidate" ] || return 0
updates_append_arch_candidate "$candidate"
case "$candidate" in
*+*)
updates_append_arch_candidate "${candidate%%+*}"
;;
esac
for suffix in _musl _uclibc _glibc -musl -uclibc -glibc .musl .uclibc .glibc; do
case "$candidate" in
*"$suffix")
base_candidate="${candidate%"$suffix"}"
updates_append_arch_candidate "$base_candidate"
;;
esac
done
}
updates_add_arch_family_fallbacks() {
local arch="$1"
updates_append_arch_candidate_variants "$arch"
case "$arch" in
aarch64_*)
updates_append_arch_candidate_variants "aarch64_generic"
;;
riscv64_*)
updates_append_arch_candidate_variants "riscv64_generic"
;;
arm_cortex-a7_neon-vfpv4)
updates_append_arch_candidate_variants "arm_cortex-a7_vfpv4"
updates_append_arch_candidate_variants "arm_cortex-a7"
;;
arm_cortex-a7_*)
updates_append_arch_candidate_variants "arm_cortex-a7"
;;
arm_cortex-a9_*)
updates_append_arch_candidate_variants "arm_cortex-a9"
;;
mipsel_24kc_24kf)
updates_append_arch_candidate_variants "mipsel_24kc"
;;
esac
}
updates_resolve_arch_candidates() {
local arch_list apk_arch_list release_arch arch
UPDATES_TARGET_ARCH=""
UPDATES_ARCH_CANDIDATES=""
if updates_is_apk; then
if [ -f /etc/apk/arch ]; then
apk_arch_list="$(tr '\r\n' ' ' </etc/apk/arch)"
[ -n "$apk_arch_list" ] && arch_list="$arch_list $apk_arch_list"
fi
apk_arch_list="$(apk --print-arch 2>/dev/null || true)"
[ -n "$apk_arch_list" ] && arch_list="$arch_list $apk_arch_list"
else
arch_list="$(opkg print-architecture 2>/dev/null | awk '$1 == "arch" && $2 !~ /^(all|noarch)$/ {print $2 " " $3}' | sort -k2,2nr | awk '{print $1}')"
fi
release_arch="$(updates_read_openwrt_release_value "DISTRIB_ARCH")"
[ -n "$release_arch" ] && arch_list="$arch_list $release_arch"
if [ -z "$(printf '%s' "$arch_list" | tr -d '[:space:]')" ]; then
arch_list="$(uname -m 2>/dev/null || true)"
fi
for arch in $arch_list; do
case "$arch" in
all | noarch)
continue
;;
esac
[ -n "$UPDATES_TARGET_ARCH" ] || UPDATES_TARGET_ARCH="$arch"
updates_add_arch_family_fallbacks "$arch"
done
[ -n "$UPDATES_TARGET_ARCH" ] || return 1
updates_log "Detected package architecture candidates: $UPDATES_ARCH_CANDIDATES"
}
updates_fetch_github_release_json() {
local owner="$1"
local repo="$2"
local response message
response="$(updates_http_get "https://api.github.com/repos/${owner}/${repo}/releases/latest" 2>/dev/null || true)"
[ -n "$response" ] || return 1
printf '%s' "$response" | jq -e . >/dev/null 2>&1 || return 1
message="$(printf '%s' "$response" | jq -r 'if type == "object" then (.message // empty) else empty end' 2>/dev/null)"
case "$message" in
*"API rate limit"* | *"rate limit exceeded"* | "Not Found")
return 1
;;
esac
printf '%s' "$response"
}
updates_fetch_github_releases_json() {
local owner="$1"
local repo="$2"
local per_page="${3:-30}"
local response message
response="$(updates_http_get "https://api.github.com/repos/${owner}/${repo}/releases?per_page=${per_page}" 2>/dev/null || true)"
[ -n "$response" ] || return 1
printf '%s' "$response" | jq -e . >/dev/null 2>&1 || return 1
message="$(printf '%s' "$response" | jq -r 'if type == "object" then (.message // empty) else empty end' 2>/dev/null)"
case "$message" in
*"API rate limit"* | *"rate limit exceeded"* | "Not Found")
return 1
;;
esac
printf '%s' "$response"
}
updates_extract_package_version() {
local package_name="$1"
case "$package_name" in
podkop-plus_*.ipk) printf '%s\n' "$package_name" | sed 's/^podkop-plus_//;s/_[^_]*\.ipk$//' ;;
podkop-plus_*.apk) printf '%s\n' "$package_name" | sed 's/^podkop-plus_//;s/\.apk$//' ;;
podkop-plus-*.ipk) printf '%s\n' "$package_name" | sed 's/^podkop-plus-//;s/-[^-]*\.ipk$//' ;;
podkop-plus-*.apk) printf '%s\n' "$package_name" | sed 's/^podkop-plus-//;s/\.apk$//' ;;
zapret_*.ipk) printf '%s\n' "$package_name" | sed 's/^zapret_//;s/_[^_]*\.ipk$//' ;;
zapret-*.apk) printf '%s\n' "$package_name" | sed 's/^zapret-//;s/\.apk$//' ;;
byedpi_*.ipk) printf '%s\n' "$package_name" | sed 's/^byedpi_//;s/_[^_]*\.ipk$//' ;;
byedpi_*.apk) printf '%s\n' "$package_name" | sed 's/^byedpi_//;s/\.apk$//' ;;
byedpi-*.ipk) printf '%s\n' "$package_name" | sed 's/^byedpi-//;s/-[^-]*\.ipk$//' ;;
byedpi-*.apk) printf '%s\n' "$package_name" | sed 's/^byedpi-//;s/\.apk$//' ;;
*) printf '%s\n' "$package_name" ;;
esac
}
updates_extract_arch_package_version() {
local package_name="$1"
local package_arch="$2"
local version
version="$(printf '%s\n' "$package_name" | sed 's/\.ipk$//;s/\.apk$//')"
case "$version" in
zapret_*) version="${version#zapret_}" ;;
zapret-*) version="${version#zapret-}" ;;
byedpi_*) version="${version#byedpi_}" ;;
byedpi-*) version="${version#byedpi-}" ;;
esac
if [ -n "$package_arch" ]; then
case "$version" in
*_$package_arch) version="${version%_$package_arch}" ;;
*-$package_arch) version="${version%-$package_arch}" ;;
esac
fi
printf '%s\n' "$version"
}
updates_extract_ipk_control_field() {
local package_file="$1"
local field_name="$2"
local control_dir value
[ -f "$package_file" ] || return 0
control_dir="$UPDATES_TMP_DIR/control.$$"
rm -rf "$control_dir"
mkdir -p "$control_dir" || return 0
if tar -xzf "$package_file" -C "$control_dir" ./control.tar.gz >/dev/null 2>&1 &&
tar -xzf "$control_dir/control.tar.gz" -C "$control_dir" ./control >/dev/null 2>&1; then
value="$(awk -v field="$field_name" '
index($0, field ":") == 1 {
sub("^[^:]*:[[:space:]]*", "")
print
exit
}
' "$control_dir/control" 2>/dev/null)"
fi
rm -rf "$control_dir"
printf '%s\n' "$value"
}
updates_extract_apk_control_field() {
local package_file="$1"
local field_name="$2"
local metadata_key
case "$field_name" in
Package) metadata_key="name" ;;
Version) metadata_key="version" ;;
*) return 0 ;;
esac
updates_command_exists apk || return 0
apk adbdump "$package_file" 2>/dev/null | sed -n "s/^ ${metadata_key}:[[:space:]]*//p" | sed -n '1p'
}
updates_extract_package_control_field() {
local package_file="$1"
local field_name="$2"
case "$package_file" in
*.ipk) updates_extract_ipk_control_field "$package_file" "$field_name" ;;
*.apk) updates_extract_apk_control_field "$package_file" "$field_name" ;;
esac
}
updates_get_openwrt_release_series() {
local release
release="$(updates_read_openwrt_release_value "DISTRIB_RELEASE")"
printf '%s\n' "$release" | sed -n 's/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p'
}
updates_resolve_zapret_release() {
local release_json candidate_name arch url
UPDATES_ZAPRET_ARCH=""
UPDATES_ZAPRET_BUNDLE_URL=""
UPDATES_ZAPRET_BUNDLE_NAME=""
UPDATES_ZAPRET_PACKAGE_VERSION=""
release_json="$(updates_fetch_github_release_json "remittor" "zapret-openwrt")" || return 1
for arch in $UPDATES_ARCH_CANDIDATES; do
candidate_name="$(printf '%s' "$release_json" | jq -r --arg arch "$arch" '[.assets[] | select(.name | endswith("_" + $arch + ".zip")) | .name][0] // empty')"
if [ -n "$candidate_name" ]; then
UPDATES_ZAPRET_ARCH="$arch"
UPDATES_ZAPRET_BUNDLE_NAME="$candidate_name"
break
fi
done
[ -n "$UPDATES_ZAPRET_BUNDLE_NAME" ] || return 1
url="$(printf '%s' "$release_json" | jq -r --arg name "$UPDATES_ZAPRET_BUNDLE_NAME" '[.assets[] | select(.name == $name) | .browser_download_url][0] // empty')"
[ -n "$url" ] || return 1
UPDATES_ZAPRET_BUNDLE_URL="$url"
UPDATES_ZAPRET_PACKAGE_VERSION="$(updates_extract_zapret_bundle_version "$UPDATES_ZAPRET_BUNDLE_NAME")"
[ -n "$UPDATES_ZAPRET_PACKAGE_VERSION" ] || UPDATES_ZAPRET_PACKAGE_VERSION="$(printf '%s\n' "$UPDATES_ZAPRET_BUNDLE_NAME" | sed 's/\.zip$//')"
}
updates_download_and_extract_zapret_package() {
local bundle_file inner_package_path
UPDATES_ZAPRET_PACKAGE_FILE=""
UPDATES_ZAPRET_PACKAGE_NAME=""
UPDATES_ZAPRET_PACKAGE_VERSION=""
bundle_file="$UPDATES_TMP_DIR/$UPDATES_ZAPRET_BUNDLE_NAME"
updates_download_with_retry "$UPDATES_ZAPRET_BUNDLE_URL" "$bundle_file" "$UPDATES_ZAPRET_BUNDLE_NAME" || return 1
if updates_is_apk; then
inner_package_path="$(unzip -l "$bundle_file" | awk '{print $4}' | grep -E '^apk/zapret-.*\.apk$' | sed -n '1p')"
else
inner_package_path="$(unzip -l "$bundle_file" | awk '{print $4}' | grep -E "^zapret_.*_${UPDATES_ZAPRET_ARCH}\.ipk$" | sed -n '1p')"
[ -n "$inner_package_path" ] || inner_package_path="$(unzip -l "$bundle_file" | awk '{print $4}' | grep -E '^zapret_.*\.ipk$' | sed -n '1p')"
fi
[ -n "$inner_package_path" ] || return 1
UPDATES_ZAPRET_PACKAGE_NAME="$(basename "$inner_package_path")"
UPDATES_ZAPRET_PACKAGE_FILE="$UPDATES_TMP_DIR/$UPDATES_ZAPRET_PACKAGE_NAME"
unzip -p "$bundle_file" "$inner_package_path" >"$UPDATES_ZAPRET_PACKAGE_FILE" || return 1
[ -s "$UPDATES_ZAPRET_PACKAGE_FILE" ] || return 1
[ -n "$UPDATES_ZAPRET_PACKAGE_VERSION" ] || UPDATES_ZAPRET_PACKAGE_VERSION="$(updates_extract_zapret_bundle_version "$UPDATES_ZAPRET_BUNDLE_NAME")"
[ -n "$UPDATES_ZAPRET_PACKAGE_VERSION" ] || UPDATES_ZAPRET_PACKAGE_VERSION="$(updates_extract_arch_package_version "$UPDATES_ZAPRET_PACKAGE_NAME" "$UPDATES_ZAPRET_ARCH")"
}
updates_resolve_byedpi_release() {
local response release_series asset_ext resolved
UPDATES_BYEDPI_ARCH=""
UPDATES_BYEDPI_PACKAGE_URL=""
UPDATES_BYEDPI_PACKAGE_NAME=""
UPDATES_BYEDPI_PACKAGE_VERSION=""
asset_ext="ipk"
updates_is_apk && asset_ext="apk"
release_series="$(updates_get_openwrt_release_series)"
response="$(updates_fetch_github_releases_json "DPITrickster" "ByeDPI-OpenWrt" 30)" || return 1
if [ -n "$release_series" ]; then
resolved="$(printf '%s' "$response" | jq -c --arg series "$release_series" '
.[]
| select(((.draft // false) | not) and ((.prerelease // false) | not))
| select(((.tag_name // "") | contains($series)) or ((.name // "") | contains($series)))
' | updates_select_byedpi_asset "$asset_ext")"
fi
if [ -z "$resolved" ]; then
resolved="$(printf '%s' "$response" | jq -c '
.[]
| select(((.draft // false) | not) and ((.prerelease // false) | not))
' | updates_select_byedpi_asset "$asset_ext")"
fi
[ -n "$resolved" ] || return 1
UPDATES_BYEDPI_ARCH="$(printf '%s\n' "$resolved" | cut -f1)"
UPDATES_BYEDPI_PACKAGE_NAME="$(printf '%s\n' "$resolved" | cut -f2)"
UPDATES_BYEDPI_PACKAGE_URL="$(printf '%s\n' "$resolved" | cut -f3)"
[ -n "$UPDATES_BYEDPI_ARCH" ] || return 1
[ -n "$UPDATES_BYEDPI_PACKAGE_NAME" ] || return 1
[ -n "$UPDATES_BYEDPI_PACKAGE_URL" ] || return 1
UPDATES_BYEDPI_PACKAGE_VERSION="$(updates_extract_arch_package_version "$UPDATES_BYEDPI_PACKAGE_NAME" "$UPDATES_BYEDPI_ARCH")"
}
updates_select_byedpi_asset() {
local asset_ext="$1"
local release_json candidate_name arch url resolved
while IFS= read -r release_json; do
[ -n "$release_json" ] || continue
[ -n "$resolved" ] && continue
for arch in $UPDATES_ARCH_CANDIDATES; do
candidate_name="$(printf '%s' "$release_json" | jq -r --arg arch "$arch" --arg ext "$asset_ext" '
[
(.assets // [])[]
| select(((.name | startswith("byedpi_")) or (.name | startswith("byedpi-"))) and (.name | endswith("." + $ext)))
| select((.name | contains("_" + $arch + "." + $ext)) or (.name | contains("-" + $arch + "." + $ext)))
| .name
][0] // empty
')"
if [ -n "$candidate_name" ]; then
url="$(printf '%s' "$release_json" | jq -r --arg name "$candidate_name" '[(.assets // [])[] | select(.name == $name) | .browser_download_url][0] // empty')"
[ -n "$url" ] || continue
resolved="$(printf '%s\t%s\t%s\n' "$arch" "$candidate_name" "$url")"
break
fi
done
done
[ -n "$resolved" ] && printf '%s\n' "$resolved"
}
updates_download_byedpi_package() {
UPDATES_BYEDPI_PACKAGE_FILE="$UPDATES_TMP_DIR/$UPDATES_BYEDPI_PACKAGE_NAME"
updates_download_with_retry "$UPDATES_BYEDPI_PACKAGE_URL" "$UPDATES_BYEDPI_PACKAGE_FILE" "$UPDATES_BYEDPI_PACKAGE_NAME" || return 1
[ -s "$UPDATES_BYEDPI_PACKAGE_FILE" ] || return 1
[ -n "$UPDATES_BYEDPI_PACKAGE_VERSION" ] || UPDATES_BYEDPI_PACKAGE_VERSION="$(updates_extract_arch_package_version "$UPDATES_BYEDPI_PACKAGE_NAME" "$UPDATES_BYEDPI_ARCH")"
}
updates_disable_standalone_zapret_service() {
[ -x /etc/init.d/zapret ] || return 0
updates_log_command "Stopping standalone zapret service" /etc/init.d/zapret stop || true
updates_log_command "Disabling standalone zapret autostart" /etc/init.d/zapret disable || true
}
updates_disable_standalone_byedpi_service() {
[ -x /etc/init.d/byedpi ] || return 0
updates_log_command "Stopping standalone byedpi service" /etc/init.d/byedpi stop || true
updates_log_command "Disabling standalone byedpi autostart" /etc/init.d/byedpi disable || true
}
updates_install_zapret() {
local action="$1"
local current_version installed normalized_current normalized_latest
updates_init_tmp_dir || updates_fail "zapret" "$action" "Failed to create temporary directory"
updates_resolve_arch_candidates || updates_fail "zapret" "$action" "Failed to detect package architecture"
updates_retry_resolve "Resolving zapret package" updates_resolve_zapret_release ||
updates_fail "zapret" "$action" "Failed to resolve zapret package for this router architecture"
installed=0
is_zapret_installed && installed=1
current_version="$(get_zapret_package_version)"
if [ "$action" = "check_update" ]; then
[ "$installed" -eq 1 ] || updates_fail "zapret" "$action" "zapret is not installed" "$current_version" "$UPDATES_ZAPRET_PACKAGE_VERSION"
normalized_current="$(updates_normalize_zapret_version "$current_version")"
normalized_latest="$(updates_normalize_zapret_version "$UPDATES_ZAPRET_PACKAGE_VERSION")"
updates_check_success_compared "zapret" "$current_version" "$UPDATES_ZAPRET_PACKAGE_VERSION" "$normalized_current" "$normalized_latest"
fi
updates_ensure_package_tool "unzip" "unzip" || updates_fail "zapret" "$action" "Failed to install unzip"
updates_download_and_extract_zapret_package || updates_fail "zapret" "$action" "Failed to download zapret package"
if ! updates_log_command "Installing zapret package $UPDATES_ZAPRET_PACKAGE_NAME" updates_pkg_install_files "$UPDATES_ZAPRET_PACKAGE_FILE"; then
updates_fail "zapret" "$action" "Failed to install zapret package" "$current_version" "$UPDATES_ZAPRET_PACKAGE_VERSION"
fi
updates_disable_standalone_zapret_service
updates_restart_podkop_after_successful_change
updates_clear_version_caches
current_version="$(get_zapret_package_version)"
updates_success "zapret" "$action" "zapret package has been installed" "$current_version" "$UPDATES_ZAPRET_PACKAGE_VERSION" 1 "latest"
}
updates_install_byedpi() {
local action="$1"
local current_version installed
updates_init_tmp_dir || updates_fail "byedpi" "$action" "Failed to create temporary directory"
updates_resolve_arch_candidates || updates_fail "byedpi" "$action" "Failed to detect package architecture"
updates_retry_resolve "Resolving ByeDPI package" updates_resolve_byedpi_release ||
updates_fail "byedpi" "$action" "Failed to resolve ByeDPI package for this router architecture"
installed=0
is_byedpi_installed && installed=1
current_version="$(get_byedpi_package_version)"
if [ "$action" = "check_update" ]; then
[ "$installed" -eq 1 ] || updates_fail "byedpi" "$action" "ByeDPI is not installed" "$current_version" "$UPDATES_BYEDPI_PACKAGE_VERSION"
updates_check_success "byedpi" "$current_version" "$UPDATES_BYEDPI_PACKAGE_VERSION"
fi
updates_download_byedpi_package || updates_fail "byedpi" "$action" "Failed to download ByeDPI package"
if ! updates_log_command "Installing ByeDPI package $UPDATES_BYEDPI_PACKAGE_NAME" updates_pkg_install_files "$UPDATES_BYEDPI_PACKAGE_FILE"; then
updates_fail "byedpi" "$action" "Failed to install ByeDPI package" "$current_version" "$UPDATES_BYEDPI_PACKAGE_VERSION"
fi
updates_disable_standalone_byedpi_service
updates_restart_podkop_after_successful_change
updates_clear_version_caches
current_version="$(get_byedpi_package_version)"
updates_success "byedpi" "$action" "ByeDPI package has been installed" "$current_version" "$UPDATES_BYEDPI_PACKAGE_VERSION" 1 "latest"
}
updates_remove_optional_component() {
local component="$1"
local action="remove"
local package_name="$2"
local label="$3"
local provider_check="$4"
local version_getter="$5"
local current_version
if ! updates_pkg_is_installed "$package_name"; then
if "$provider_check"; then
updates_fail "$component" "$action" "$label exists outside the package manager and was not removed automatically"
fi
updates_success "$component" "$action" "$label is already removed" "" "" 0
fi
current_version="$("$version_getter")"
if ! updates_log_command "Removing $label package" updates_pkg_remove_name "$package_name"; then
updates_fail "$component" "$action" "Failed to remove $label package" "$current_version"
fi
updates_clear_version_caches
if "$provider_check"; then
updates_fail "$component" "$action" "$label package was removed, but provider files are still present" "$current_version"
fi
updates_restart_podkop_after_successful_change
updates_success "$component" "$action" "$label package has been removed" "$current_version" "" 1
}
updates_normalize_sing_box_version() {
printf '%s\n' "$1" |
sed 's/^v//;s/+.*$//;s/[[:space:]].*$//'
}
updates_extract_zapret_bundle_version() {
local bundle_name="$1"
local version
version="$(basename "$bundle_name" | sed -n 's/^zapret_v\([^_][^_]*\)_.*/\1/p')"
[ -n "$version" ] || version="$(basename "$bundle_name" | sed -n 's/^zapret_\([^_][^_]*\)_.*/\1/p')"
printf '%s\n' "$version" | sed 's/^v//'
}
updates_normalize_zapret_version() {
printf '%s\n' "$1" |
sed 's/^v//;s/-r[0-9][0-9]*$//;s/+.*$//;s/[[:space:]].*$//'
}
updates_version_newer_sort() {
local candidate="$1"
local installed="$2"
local newest
[ -n "$candidate" ] || return 1
[ -n "$installed" ] || return 1
[ "$candidate" != "$installed" ] || return 1
newest="$(printf '%s\n%s\n' "$installed" "$candidate" | sort -V | tail -n 1)"
[ "$newest" = "$candidate" ]
}
updates_resolve_sing_box_extended_arch_suffix() {
local host_arch distrib_arch
host_arch="$(uname -m 2>/dev/null || true)"
distrib_arch="$(updates_read_openwrt_release_value "DISTRIB_ARCH")"
case "$distrib_arch" in
*mipsel* | *mipsle*) host_arch="mipsel" ;;
*mips64el* | *mips64le*) host_arch="mips64el" ;;
esac
case "$host_arch" in
aarch64) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="arm64" ;;
armv7*) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="armv7" ;;
armv6*) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="armv6" ;;
x86_64) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="amd64" ;;
i386 | i686) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="386" ;;
mips) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="mips-softfloat" ;;
mipsel | mipsle) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="mipsle-softfloat" ;;
mips64) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="mips64" ;;
mips64el | mips64le) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="mips64le" ;;
riscv64) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="riscv64" ;;
s390x) UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX="s390x" ;;
*) return 1 ;;
esac
}
updates_resolve_sing_box_extended_release() {
local response tag release_json asset_pattern
UPDATES_SING_BOX_EXTENDED_RELEASE_TAG=""
UPDATES_SING_BOX_EXTENDED_ASSET_URL=""
UPDATES_SING_BOX_EXTENDED_ASSET_NAME=""
updates_resolve_sing_box_extended_arch_suffix || return 1
response="$(updates_fetch_github_releases_json "shtorm-7" "sing-box-extended" 30)" || return 1
tag="$(printf '%s' "$response" | jq -r '
[
.[]
| select(((.prerelease // false) | not) and ((.draft // false) | not))
| .tag_name // empty
| select(((ascii_downcase | contains("alpha")) or (ascii_downcase | contains("beta")) or (ascii_downcase | contains("rc"))) | not)
][0] // empty
')"
[ -n "$tag" ] || return 1
UPDATES_SING_BOX_EXTENDED_RELEASE_TAG="$tag"
release_json="$(printf '%s' "$response" | jq -c --arg tag "$tag" '[.[] | select(.tag_name == $tag)][0] // empty')"
asset_pattern="linux-${UPDATES_SING_BOX_EXTENDED_ARCH_SUFFIX}.tar.gz"
UPDATES_SING_BOX_EXTENDED_ASSET_URL="$(printf '%s' "$release_json" | jq -r --arg pattern "$asset_pattern" '
[(.assets // [])[] | select(.name | endswith($pattern)) | .browser_download_url][0] // empty
')"
[ -n "$UPDATES_SING_BOX_EXTENDED_ASSET_URL" ] || return 1
UPDATES_SING_BOX_EXTENDED_ASSET_NAME="$(basename "$UPDATES_SING_BOX_EXTENDED_ASSET_URL")"
}
updates_install_sing_box_extended() {
local action="$1"
local current_version latest_version normalized_current normalized_latest archive_file binary_path target_binary extract_error new_version
updates_init_tmp_dir || updates_fail "sing_box" "$action" "Failed to create temporary directory"
current_version="$(get_sing_box_version)"
updates_resolve_sing_box_extended_release || updates_fail "sing_box" "$action" "Failed to resolve sing-box-extended release" "$current_version"
latest_version="$(updates_normalize_sing_box_version "$UPDATES_SING_BOX_EXTENDED_RELEASE_TAG")"
normalized_current="$(updates_normalize_sing_box_version "$current_version")"
normalized_latest="$(updates_normalize_sing_box_version "$latest_version")"
if [ "$action" = "check_update" ]; then
is_sing_box_extended "$current_version" || updates_fail "sing_box" "$action" "sing-box-extended is not installed" "$current_version" "$latest_version"
updates_check_success "sing_box" "$normalized_current" "$normalized_latest"
fi
archive_file="$UPDATES_TMP_DIR/$UPDATES_SING_BOX_EXTENDED_ASSET_NAME"
updates_download_with_retry "$UPDATES_SING_BOX_EXTENDED_ASSET_URL" "$archive_file" "$UPDATES_SING_BOX_EXTENDED_ASSET_NAME" ||
updates_fail "sing_box" "$action" "Failed to download sing-box-extended" "$current_version" "$latest_version"
binary_path="$(tar -tzf "$archive_file" 2>/dev/null | grep -E '(^|/)sing-box$' | sed -n '1p')"
[ -n "$binary_path" ] || updates_fail "sing_box" "$action" "sing-box binary was not found in the downloaded archive" "$current_version" "$latest_version"
target_binary="/usr/bin/.sing-box.new.$$"
extract_error="$UPDATES_TMP_DIR/sing-box-extract.err"
if ! tar -xzf "$archive_file" -O "$binary_path" >"$target_binary" 2>"$extract_error"; then
while IFS= read -r line; do
[ -n "$line" ] && updates_log "$line" "error"
done <"$extract_error"
rm -f "$target_binary"
updates_fail "sing_box" "$action" "Failed to extract sing-box-extended" "$current_version" "$latest_version"
fi
if [ ! -s "$target_binary" ]; then
rm -f "$target_binary"
updates_fail "sing_box" "$action" "sing-box binary was empty after extraction" "$current_version" "$latest_version"
fi
if ! chmod 0755 "$target_binary"; then
rm -f "$target_binary"
updates_fail "sing_box" "$action" "Failed to prepare sing-box-extended binary" "$current_version" "$latest_version"
fi
if ! mv -f "$target_binary" /usr/bin/sing-box; then
rm -f "$target_binary"
updates_fail "sing_box" "$action" "Failed to install sing-box-extended" "$current_version" "$latest_version"
fi
updates_restart_podkop_after_successful_change
updates_clear_version_caches
new_version="$(get_sing_box_version)"
updates_log "Installed sing-box-extended ${new_version:-unknown}"
updates_success "sing_box" "$action" "sing-box-extended has been installed" "$new_version" "$latest_version" 1 "latest"
}
updates_install_stable_sing_box() {
local action="$1"
local current_version latest_version new_version changed
current_version="$(updates_get_installed_package_version "sing-box")"
[ -n "$current_version" ] || current_version="$(get_sing_box_version)"
latest_version="$(updates_get_available_package_version "sing-box")"
[ -n "$latest_version" ] || latest_version="$(updates_get_installed_package_version "sing-box")"
[ -n "$latest_version" ] || updates_fail "sing_box" "$action" "Failed to resolve stable sing-box package version" "$current_version"
if [ "$action" = "check_update" ]; then
updates_check_success "sing_box" "$current_version" "$latest_version"
fi
updates_log_command "Updating package lists before sing-box installation" updates_pkg_list_update ||
updates_fail "sing_box" "$action" "Failed to update package lists" "$current_version" "$latest_version"
latest_version="$(updates_get_available_package_version "sing-box")"
[ -n "$latest_version" ] || latest_version="$(updates_get_installed_package_version "sing-box")"
[ -n "$latest_version" ] || updates_fail "sing_box" "$action" "Failed to resolve stable sing-box package version" "$current_version"
if ! updates_log_command "Installing stable sing-box package" updates_pkg_install_name_downgrade "sing-box"; then
updates_fail "sing_box" "$action" "Failed to install stable sing-box" "$current_version" "$latest_version"
fi
updates_restart_podkop_after_successful_change
updates_clear_version_caches
new_version="$(get_sing_box_version)"
changed=1
[ "$new_version" = "$current_version" ] && changed=0
updates_success "sing_box" "$action" "stable sing-box has been installed" "$new_version" "$latest_version" "$changed" "latest"
}
updates_run_podkop_update_installer() {
local latest_version="$1"
local action="${2:-install}"
local runner
runner="/tmp/podkop-plus-update-runner.$$"
cat >"$runner" <<'EOF'
#!/bin/sh
repo="$1"
latest_version="$2"
current_version="$3"
action="${4:-install}"
podkop_was_running="${5:-1}"
tmp_dir="$(mktemp -d /tmp/podkop-plus-self-update.XXXXXX 2>/dev/null || true)"
[ -n "$tmp_dir" ] || tmp_dir="/tmp/podkop-plus-self-update.$$"
mkdir -p "$tmp_dir" || exit 1
log_line() {
logger -t "podkop-plus" "[$2] Updates: $1"
}
json_response() {
jq -cn \
--argjson success "$1" \
--arg component "podkop" \
--arg action "$action" \
--arg message "$2" \
--arg current_version "$3" \
--arg latest_version "$4" \
--argjson changed "$5" \
--arg status "$6" \
'{
success: $success,
component: $component,
action: $action,
message: $message,
current_version: $current_version,
latest_version: $latest_version,
changed: $changed,
status: $status
}'
}
download_file() {
if command -v curl >/dev/null 2>&1; then
curl --connect-timeout 5 -m 60 -fsSL "$1" -o "$2"
return $?
fi
if command -v wget >/dev/null 2>&1; then
wget -T 60 -q -O "$2" "$1"
return $?
fi
return 1
}
cleanup() {
rm -rf "$tmp_dir"
rm -f "$0"
}
installer="$tmp_dir/install.sh"
output="$tmp_dir/install.log"
installer_url="https://raw.githubusercontent.com/$repo/main/install.sh"
trap cleanup EXIT HUP INT TERM
log_line "Downloading Podkop Plus installer from $installer_url" "info"
if ! download_file "$installer_url" "$installer" || [ ! -s "$installer" ]; then
log_line "Failed to download Podkop Plus installer" "error"
json_response false "Failed to download Podkop Plus installer" "$current_version" "$latest_version" 0 ""
exit 1
fi
chmod 0755 "$installer" 2>/dev/null || true
log_line "Running Podkop Plus update to $latest_version" "info"
if sh "$installer" >"$output" 2>&1; then
status=0
else
status=$?
fi
while IFS= read -r line; do
[ -n "$line" ] && log_line "$line" "info"
done <"$output"
rm -f /tmp/podkop-plus.latest-version.cache
rm -f /var/run/podkop-plus/system-info.json
rm -f /tmp/podkop-plus/system-info.json
if [ "$status" -ne 0 ]; then
tail_message="$(tail -n 5 "$output" 2>/dev/null | tr '\n' ' ' | cut -c1-240)"
[ -n "$tail_message" ] || tail_message="Podkop Plus update failed"
log_line "$tail_message" "error"
json_response false "$tail_message" "$current_version" "$latest_version" 0 ""
exit "$status"
fi
if [ "$podkop_was_running" = "1" ] && [ -x /etc/init.d/podkop-plus ]; then
log_line "Restarting Podkop Plus after successful component change" "info"
/etc/init.d/podkop-plus restart >/dev/null 2>&1 || log_line "Podkop Plus restart command failed" "warn"
else
log_line "Podkop Plus was not running before component change; restart skipped" "info"
fi
new_version="$(/usr/bin/podkop-plus show_version 2>/dev/null || true)"
[ -n "$new_version" ] || new_version="$latest_version"
log_line "Podkop Plus updated to $new_version" "info"
json_response true "Podkop Plus has been installed" "$new_version" "$latest_version" 1 "latest"
exit 0
EOF
chmod 0755 "$runner" || updates_fail "podkop" "$action" "Failed to prepare update runner" "$PODKOP_VERSION" "$latest_version"
exec sh "$runner" "$PODKOP_RELEASE_REPO" "$latest_version" "$PODKOP_VERSION" "$action" "$UPDATES_PODKOP_WAS_RUNNING"
}
updates_check_podkop_plus() {
local latest_version compare_result status message now
latest_version="$(fetch_latest_podkop_version)"
[ -n "$latest_version" ] || latest_version="unknown"
if [ "$latest_version" = "unknown" ]; then
updates_fail "podkop" "check_update" "Failed to check Podkop Plus updates" "$PODKOP_VERSION" "$latest_version"
fi
now="$(date +%s 2>/dev/null)"
case "$now" in
'' | *[!0-9]*) now=0 ;;
esac
write_podkop_latest_version_cache "$latest_version" "$now"
if ! is_podkop_release_version "$PODKOP_VERSION"; then
updates_log "Podkop Plus current version is not a release version ($PODKOP_VERSION)"
updates_success "podkop" "check_update" "Installed version is newer than release" "$PODKOP_VERSION" "$latest_version" 0 "dev"
fi
compare_result="$(podkop_release_version_compare "$PODKOP_VERSION" "$latest_version" 2>/dev/null || true)"
if [ -z "$compare_result" ]; then
updates_fail "podkop" "check_update" "Failed to compare Podkop Plus versions" "$PODKOP_VERSION" "$latest_version"
fi
status="$(updates_status_from_compare "$compare_result")" || updates_fail "podkop" "check_update" "Failed to compare Podkop Plus versions" "$PODKOP_VERSION" "$latest_version"
case "$status" in
latest)
message="Latest version is installed"
updates_log "Podkop Plus is already up to date ($PODKOP_VERSION)"
;;
outdated)
message="Update is available"
updates_log "Podkop Plus update found: $PODKOP_VERSION -> $latest_version"
;;
dev)
message="Installed version is newer than release"
updates_log "Podkop Plus installed version is newer than upstream release: $PODKOP_VERSION -> $latest_version"
;;
esac
updates_success "podkop" "check_update" "$message" "$PODKOP_VERSION" "$latest_version" 0 "$status"
}
updates_install_podkop_plus() {
local latest_version now
latest_version="$(fetch_latest_podkop_version)"
[ -n "$latest_version" ] || latest_version="unknown"
if [ "$latest_version" = "unknown" ]; then
updates_fail "podkop" "install" "Failed to resolve Podkop Plus release" "$PODKOP_VERSION" "$latest_version"
fi
now="$(date +%s 2>/dev/null)"
case "$now" in
'' | *[!0-9]*) now=0 ;;
esac
write_podkop_latest_version_cache "$latest_version" "$now"
updates_log "Installing Podkop Plus release $latest_version"
updates_run_podkop_update_installer "$latest_version" "install"
}
component_action() {
local component="$1"
local action="$2"
trap updates_component_action_cleanup EXIT HUP INT TERM
updates_acquire_component_lock || updates_fail "${component:-unknown}" "${action:-unknown}" "Another component action is already running"
updates_capture_podkop_running_state
case "$component:$action" in
podkop:check_update)
updates_check_podkop_plus
;;
podkop:install)
updates_install_podkop_plus
;;
sing_box:check_update)
if is_sing_box_extended "$(get_sing_box_version)"; then
updates_install_sing_box_extended "$action"
fi
updates_install_stable_sing_box "$action"
;;
sing_box:install)
if is_sing_box_extended "$(get_sing_box_version)"; then
updates_install_sing_box_extended "$action"
fi
updates_install_stable_sing_box "$action"
;;
sing_box:install_extended)
updates_install_sing_box_extended "$action"
;;
sing_box:install_stable)
updates_install_stable_sing_box "$action"
;;
zapret:check_update | zapret:install)
updates_install_zapret "$action"
;;
zapret:remove)
updates_remove_optional_component "zapret" "zapret" "zapret" is_zapret_installed get_zapret_package_version
;;
byedpi:check_update | byedpi:install)
updates_install_byedpi "$action"
;;
byedpi:remove)
updates_remove_optional_component "byedpi" "byedpi" "ByeDPI" is_byedpi_installed get_byedpi_package_version
;;
*)
updates_fail "${component:-unknown}" "${action:-unknown}" "Unknown component action"
;;
esac
}