From e1c9ee49f0893836ecec64229d2a463e15455a84 Mon Sep 17 00:00:00 2001 From: Arvin <62139570+wnlen@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:20:31 +0800 Subject: [PATCH] fix: restore subscription update compatibility --- README.md | 2 + scripts/core/alias.sh | 4 + scripts/core/clashctl.sh | 19 ++- scripts/core/common.sh | 2 + scripts/core/completion.sh | 10 +- scripts/dev/check-sub-update-compat.sh | 109 ++++++++++++++++++ .../check-uninstall-system-proxy-failure.sh | 57 +++++++++ uninstall.sh | 10 +- 8 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 scripts/dev/check-sub-update-compat.sh create mode 100644 scripts/dev/check-uninstall-system-proxy-failure.sh diff --git a/README.md b/README.md index 944f575..d18f7e6 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ OpenWrt 下 root/system 安装会把 `clashctl`、`clashon`、`clashoff` 等命 clashctl add <订阅链接> <名称> clashctl use clashctl ls +clashctl sub update +clashsub update # 兼容旧版命令 clashctl sub clashctl sub list clashctl sub enable <名称> diff --git a/scripts/core/alias.sh b/scripts/core/alias.sh index db3c069..63a9858 100644 --- a/scripts/core/alias.sh +++ b/scripts/core/alias.sh @@ -396,6 +396,10 @@ clashls() { clashctl ls "$@" } +clashsub() { + clashctl sub "$@" +} + clashselect() { clashctl select "$@" } diff --git a/scripts/core/clashctl.sh b/scripts/core/clashctl.sh index 5bf9009..05d9e70 100644 --- a/scripts/core/clashctl.sh +++ b/scripts/core/clashctl.sh @@ -39,9 +39,9 @@ Usage: 📦 Subscription: config show 📡 查看当前订阅 - config regen 🔄 更新当前订阅 + sub update 🔄 更新当前订阅(兼容:clashsub update) ls 📜 查看订阅列表 - sub 📡 订阅高级管理(启用 / 禁用 / 重命名 / 删除) + sub 📡 订阅管理(更新 / 启用 / 禁用 / 重命名 / 删除) 🕹️ Control: clashui 🕹️ 查看 Web 控制台 @@ -69,6 +69,7 @@ Usage: completion 💡 导出 Bash / Zsh 补全脚本 📌 Advanced Examples: + clashctl sub update clashctl sub list clashctl sub enable hk clashctl sub disable hk @@ -7047,6 +7048,12 @@ cmd_sub() { prepare case "${1:-}" in + update) + regenerate_config + apply_runtime_change_after_config_mutation + print_config_regen_feedback + print_config_apply_feedback + ;; list) shift || true cmd_ls "$@" @@ -7101,8 +7108,9 @@ cmd_sub() { cmd_health "$@" ;; "") - ui_title "📡 订阅高级管理" + ui_title "📡 订阅管理" echo "📜 用法:" + echo " clashctl sub update" echo " clashctl sub list" echo " clashctl sub use <名称>" echo " clashctl sub enable <名称>" @@ -7112,11 +7120,12 @@ cmd_sub() { echo " clashctl sub health [名称]" echo echo "🧩 说明:" - echo " add / use / ls 属于主路径" + echo " add / use / ls / sub update 属于主路径" echo " health 保留为多订阅健康审计" - echo " sub 仅用于高级维护操作" + echo " 其他 sub 子命令用于高级维护操作" echo echo "💡 常用动作:" + echo " clashctl sub update" echo " clashctl sub list" echo " clashctl sub health" echo " clashctl sub enable <名称>" diff --git a/scripts/core/common.sh b/scripts/core/common.sh index 7480832..4754712 100644 --- a/scripts/core/common.sh +++ b/scripts/core/common.sh @@ -2599,6 +2599,7 @@ install_alias_command_wrappers() { clashon \ clashoff \ clashproxy \ + clashsub \ clashls \ clashselect \ clashui \ @@ -2897,6 +2898,7 @@ remove_alias_command_wrappers() { clashon \ clashoff \ clashproxy \ + clashsub \ clashls \ clashselect \ clashui \ diff --git a/scripts/core/completion.sh b/scripts/core/completion.sh index 3f9fdc2..7998be0 100644 --- a/scripts/core/completion.sh +++ b/scripts/core/completion.sh @@ -224,7 +224,7 @@ _clash_for_linux_complete_sub() { COMPREPLY=() if [ "$rel_index" -eq 1 ]; then - _clash_for_linux_add_matches "$cur" list use set enable disable rename remove rm del health help -h --help + _clash_for_linux_add_matches "$cur" update list use set enable disable rename remove rm del health help -h --help return 0 fi @@ -339,6 +339,13 @@ _clash_for_linux_complete_command() { arg2="${COMP_WORDS[3]:-}" arg3="${COMP_WORDS[4]:-}" ;; + clashsub) + canonical="sub" + rel_index=$COMP_CWORD + arg1="${COMP_WORDS[1]:-}" + arg2="${COMP_WORDS[2]:-}" + arg3="${COMP_WORDS[3]:-}" + ;; clashrelay) canonical="relay" rel_index=$COMP_CWORD @@ -396,6 +403,7 @@ _clash_for_linux_complete_command() { } complete -F _clash_for_linux_complete_command clashctl +complete -F _clash_for_linux_complete_command clashsub complete -F _clash_for_linux_complete_command clashrelay complete -F _clash_for_linux_complete_command clashmixin complete -F _clash_for_linux_complete_command clashsecret diff --git a/scripts/dev/check-sub-update-compat.sh b/scripts/dev/check-sub-update-compat.sh new file mode 100644 index 0000000..5dee5ff --- /dev/null +++ b/scripts/dev/check-sub-update-compat.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +source_clashctl_for_tests() { + set -- "" + # Source the real command functions; suppress the no-arg usage printed by the dispatcher. + source "$PROJECT_DIR/scripts/core/clashctl.sh" >/dev/null +} + +source_clashctl_for_tests + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +calls_file="$tmp_dir/sub-update-calls" +output_file="$tmp_dir/sub-update-output" + +prepare() { :; } +regenerate_config() { printf 'regenerate\n' >> "$calls_file"; } +apply_runtime_change_after_config_mutation() { printf 'apply\n' >> "$calls_file"; } +print_config_regen_feedback() { printf 'regen-feedback\n' >> "$calls_file"; } +print_config_apply_feedback() { printf 'apply-feedback\n' >> "$calls_file"; } + +: > "$calls_file" +if ! ( cmd_sub update ) > "$output_file" 2>&1; then + echo "not ok - clashctl sub update should succeed" >&2 + sed 's/^/ /' "$output_file" >&2 + exit 1 +fi + +expected_calls="$(printf '%s\n' regenerate apply regen-feedback apply-feedback)" +actual_calls="$(cat "$calls_file")" +if [ "$actual_calls" != "$expected_calls" ]; then + echo "not ok - clashctl sub update should follow the config regen path" >&2 + printf 'expected:\n%s\nactual:\n%s\n' "$expected_calls" "$actual_calls" >&2 + exit 1 +fi + +echo "ok - clashctl sub update follows the config regen path" + +# Verify both the sourced-shell compatibility function and the installed command wrapper. +# shellcheck source=../core/common.sh +source "$PROJECT_DIR/scripts/core/common.sh" + +fixture_bin="$tmp_dir/bin" +mkdir -p "$fixture_bin" + +PROJECT_DIR="$PROJECT_DIR" +alias_source_file() { printf '%s\n' "$PROJECT_DIR/scripts/core/alias.sh"; } +command_install_dir() { printf '%s\n' "$fixture_bin"; } + +install_alias_command_wrappers + +if [ ! -x "$fixture_bin/clashsub" ]; then + echo "not ok - install should create the clashsub compatibility wrapper" >&2 + exit 1 +fi + +cat > "$fixture_bin/clashctl-bin" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" +EOF +chmod +x "$fixture_bin/clashctl-bin" + +wrapper_output="$( + env -i \ + HOME="$tmp_dir/home" \ + PATH="$fixture_bin:/usr/bin:/bin" \ + CLASH_SHELL_AUTO_RESTORE_PROXY=false \ + "$fixture_bin/clashsub" update +)" + +if [ "$wrapper_output" != "sub update" ]; then + echo "not ok - clashsub update should delegate to clashctl sub update" >&2 + printf 'actual: %s\n' "$wrapper_output" >&2 + exit 1 +fi + +remove_alias_command_wrappers +if [ -e "$fixture_bin/clashsub" ]; then + echo "not ok - uninstall should remove the clashsub compatibility wrapper" >&2 + exit 1 +fi + +echo "ok - clashsub update compatibility wrapper is installed and removed" + +completion_file="$tmp_dir/clashctl-completion.bash" +bash "$PROJECT_DIR/scripts/core/clashctl.sh" completion bash > "$completion_file" + +completion_output="$( + bash --noprofile --norc -c ' + source "$1" + COMP_WORDS=(clashsub u) + COMP_CWORD=1 + _clash_for_linux_complete_command + printf "%s\n" "${COMPREPLY[@]}" + ' bash "$completion_file" +)" + +if ! printf '%s\n' "$completion_output" | grep -Fxq update; then + echo "not ok - clashsub should complete the update subcommand" >&2 + printf 'actual: %s\n' "$completion_output" >&2 + exit 1 +fi + +echo "ok - clashsub update is available in shell completion" diff --git a/scripts/dev/check-uninstall-system-proxy-failure.sh b/scripts/dev/check-uninstall-system-proxy-failure.sh new file mode 100644 index 0000000..b17c022 --- /dev/null +++ b/scripts/dev/check-uninstall-system-proxy-failure.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +test_project="$tmp_dir/project" +test_home="$tmp_dir/home" +output_file="$tmp_dir/uninstall-output" + +mkdir -p "$test_project" "$test_home" "$test_project/runtime" +cp "$PROJECT_DIR/uninstall.sh" "$test_project/uninstall.sh" +cp -R "$PROJECT_DIR/scripts" "$test_project/scripts" +cp -R "$PROJECT_DIR/config" "$test_project/config" +cp "$PROJECT_DIR/.env" "$test_project/.env" + +# Override external effects in the isolated fixture while preserving the real +# uninstall control flow and forcing the exact /etc/environment cleanup failure. +cat >> "$test_project/scripts/init/script.sh" <<'EOF' +boot_proxy_keep_disable() { return 2; } +system_proxy_env_file() { printf '%s\n' /etc/environment; } +clear_shell_proxy_persist_state() { :; } +stop_subconverter() { :; } +service_stop() { :; } +stop_runtime() { :; } +remove_runtime_entry() { :; } +remove_clashctl_entry() { :; } +remove_clashctl_completion() { :; } +remove_shell_alias_entry() { :; } +clear_controller_secret() { :; } +EOF + +if env -i \ + HOME="$test_home" \ + PATH="/usr/bin:/bin" \ + bash "$test_project/uninstall.sh" > "$output_file" 2>&1; then + echo "not ok - uninstall should fail when /etc/environment cleanup fails" >&2 + sed 's/^/ /' "$output_file" >&2 + exit 1 +fi + +if ! grep -Fq '[error]' "$output_file" || ! grep -Fq '/etc/environment' "$output_file"; then + echo "not ok - uninstall should clearly report the system proxy cleanup error" >&2 + sed 's/^/ /' "$output_file" >&2 + exit 1 +fi + +if grep -Fq '[ok] 卸载完成' "$output_file"; then + echo "not ok - uninstall must not claim success after system proxy cleanup fails" >&2 + sed 's/^/ /' "$output_file" >&2 + exit 1 +fi + +echo "ok - uninstall fails clearly when /etc/environment cleanup fails" diff --git a/uninstall.sh b/uninstall.sh index 1c27492..308fb43 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -154,10 +154,11 @@ if [ "$KEEP_RUNTIME" != "true" ]; then echo "[ok] 已删除运行目录:$RUNTIME_DIR" if [ "$SYSTEM_PROXY_CLOSED" = "true" ]; then echo "[ok] 已关闭系统代理持久接管" + echo "[ok] 完整卸载:已清理服务、入口、运行目录与 controller secret" else - echo "[warn] 未能确认系统代理持久块已关闭,请检查:$(system_proxy_env_file)" + echo "[error] 系统代理持久块清理失败:$(system_proxy_env_file)" >&2 + echo "[info] 已清理服务、入口、运行目录与 controller secret,但系统代理仍需处理" fi - echo "[ok] 完整卸载:已清理服务、入口、运行目录与 controller secret" print_uninstall_modes_hint elif [ "$DEV_RESET" = "true" ]; then cache_backup_dir="$(mktemp -d)" @@ -200,6 +201,11 @@ else echo "[info] 保留内容:runtime 数据仍在(按 --keep-runtime 请求)" fi +if [ "$SYSTEM_PROXY_CLOSED" != "true" ]; then + echo "[error] 卸载未完成:请使用有权限的用户重新执行此脚本,或手动删除 $(system_proxy_env_file) 中的 clash-for-linux 代理块" >&2 + exit 1 +fi + echo "[ok] 卸载完成" if [ "$REMOVE_PROJECT" = "true" ]; then