mirror of
https://github.com/wnlen/clash-for-linux.git
synced 2026-10-10 12:13:04 +08:00
Fix: Converge the clashctl CLI and address issues with subscriptions, UI, and the proxy main link.
This commit is contained in:
+119
-38
@@ -9,6 +9,33 @@ _clashctl_real() {
|
||||
command clashctl "$@"
|
||||
}
|
||||
|
||||
_clashctl_real_on() {
|
||||
local project_dir clashctl_script
|
||||
|
||||
project_dir="$(_clash_alias_project_dir)"
|
||||
clashctl_script="$project_dir/scripts/core/clashctl.sh"
|
||||
|
||||
if [ -f "$clashctl_script" ]; then
|
||||
bash "$clashctl_script" on "$@"
|
||||
return $?
|
||||
fi
|
||||
|
||||
_clashctl_real on "$@"
|
||||
}
|
||||
|
||||
_clashctl_real_on_target() {
|
||||
local project_dir clashctl_script
|
||||
|
||||
project_dir="$(_clash_alias_project_dir)"
|
||||
clashctl_script="$project_dir/scripts/core/clashctl.sh"
|
||||
|
||||
if [ -f "$clashctl_script" ]; then
|
||||
echo "bash $clashctl_script on"
|
||||
else
|
||||
echo "_clashctl_real on"
|
||||
fi
|
||||
}
|
||||
|
||||
_clash_alias_project_dir() {
|
||||
local self_dir
|
||||
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
@@ -46,6 +73,7 @@ _clash_alias_print_sep() {
|
||||
|
||||
_clash_alias_proxy_on() {
|
||||
_clashctl_real proxy on >/dev/null || return $?
|
||||
_clash_alias_export_system_proxy || return $?
|
||||
}
|
||||
|
||||
_clash_alias_proxy_off() {
|
||||
@@ -56,6 +84,34 @@ _clash_alias_proxy_show() {
|
||||
_clashctl_real proxy show 2>/dev/null || true
|
||||
}
|
||||
|
||||
_clash_alias_export_system_proxy() {
|
||||
local proxy_file http_url https_url all_url no_proxy
|
||||
|
||||
proxy_file="${SYSTEM_PROXY_ENV_FILE:-/etc/environment}"
|
||||
[ -f "$proxy_file" ] || return 1
|
||||
grep -Fq "# >>> clash-for-linux system proxy >>>" "$proxy_file" 2>/dev/null || return 1
|
||||
|
||||
http_url="$(sed -nE 's/^http_proxy="?([^"\r\n]+)"?$/\1/p' "$proxy_file" | tail -n 1)"
|
||||
https_url="$(sed -nE 's/^https_proxy="?([^"\r\n]+)"?$/\1/p' "$proxy_file" | tail -n 1)"
|
||||
all_url="$(sed -nE 's/^all_proxy="?([^"\r\n]+)"?$/\1/p' "$proxy_file" | tail -n 1)"
|
||||
no_proxy="$(sed -nE 's/^NO_PROXY="?([^"\r\n]+)"?$/\1/p' "$proxy_file" | tail -n 1)"
|
||||
[ -n "${no_proxy:-}" ] || no_proxy="$(sed -nE 's/^no_proxy="?([^"\r\n]+)"?$/\1/p' "$proxy_file" | tail -n 1)"
|
||||
|
||||
[ -n "${http_url:-}" ] || return 1
|
||||
[ -n "${https_url:-}" ] || https_url="$http_url"
|
||||
[ -n "${all_url:-}" ] || all_url="${http_url/http:\/\//socks5://}"
|
||||
[ -n "${no_proxy:-}" ] || no_proxy="127.0.0.1,localhost,::1"
|
||||
|
||||
export http_proxy="$http_url"
|
||||
export https_proxy="$https_url"
|
||||
export HTTP_PROXY="$http_url"
|
||||
export HTTPS_PROXY="$https_url"
|
||||
export all_proxy="$all_url"
|
||||
export ALL_PROXY="$all_url"
|
||||
export no_proxy="$no_proxy"
|
||||
export NO_PROXY="$no_proxy"
|
||||
}
|
||||
|
||||
_clash_alias_unset_shell_proxy() {
|
||||
unset \
|
||||
http_proxy https_proxy HTTP_PROXY HTTPS_PROXY \
|
||||
@@ -75,69 +131,94 @@ _clash_alias_prepare_on() {
|
||||
|
||||
_clash_alias_after_on() {
|
||||
_clash_alias_set_persist_enabled "true"
|
||||
_clash_alias_proxy_on || return $?
|
||||
_clash_alias_export_system_proxy || return $?
|
||||
|
||||
_clash_alias_print_sep
|
||||
if [ "${CLASH_WRAPPER_EXEC:-0}" = "1" ]; then
|
||||
echo "✅ 系统代理已开启"
|
||||
else
|
||||
echo "✅ 当前 Shell 代理环境已同步"
|
||||
fi
|
||||
_clash_alias_proxy_show
|
||||
|
||||
if [ "${CLASH_WRAPPER_EXEC:-0}" = "1" ]; then
|
||||
echo "⚠️ 当前通过独立命令执行,Shell 变量不会自动回写到父终端"
|
||||
echo '💡 请在当前终端执行:eval "$(clashctl proxy on)"'
|
||||
echo "💡 如需当前终端立即生效,请重新打开终端或 source shell 入口后执行 clashon"
|
||||
fi
|
||||
|
||||
echo "👉 下一步:$(_clash_alias_status_next)"
|
||||
}
|
||||
|
||||
_clash_alias_after_off() {
|
||||
_clash_alias_set_persist_enabled "false"
|
||||
_clash_alias_print_sep
|
||||
echo "🧹 系统代理已关闭"
|
||||
}
|
||||
|
||||
_clash_alias_run_on() {
|
||||
local on_output on_rc had_errexit
|
||||
|
||||
_clash_alias_prepare_on || return $?
|
||||
|
||||
_clashctl_real on "$@" || return $?
|
||||
echo "🟢 正在开启代理..."
|
||||
|
||||
_clash_alias_after_on || return $?
|
||||
on_output="$(mktemp "${TMPDIR:-/tmp}/clashon.XXXXXX")" || {
|
||||
echo "🔴 开启代理失败:无法创建临时输出文件" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
had_errexit="false"
|
||||
case "$-" in
|
||||
*e*)
|
||||
had_errexit="true"
|
||||
set +e
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASH_ALIAS_CALL=1 _clashctl_real_on "$@" >"$on_output" 2>&1
|
||||
on_rc=$?
|
||||
|
||||
if [ "$had_errexit" = "true" ]; then
|
||||
set -e
|
||||
fi
|
||||
|
||||
if [ "$on_rc" -ne 0 ]; then
|
||||
if _clash_alias_export_system_proxy; then
|
||||
echo "⚠️ clashctl on 返回非 0,但系统代理已写入,继续同步当前 Shell(底层返回码:$on_rc)" >&2
|
||||
if [ -s "$on_output" ]; then
|
||||
sed 's/^/ /' "$on_output" >&2
|
||||
fi
|
||||
elif _clash_alias_proxy_on; then
|
||||
echo "⚠️ clashctl on 返回非 0,已通过 proxy on 继续同步当前 Shell(底层返回码:$on_rc)" >&2
|
||||
if [ -s "$on_output" ]; then
|
||||
sed 's/^/ /' "$on_output" >&2
|
||||
fi
|
||||
else
|
||||
echo "🔴 开启代理失败(底层返回码:$on_rc)" >&2
|
||||
if [ -s "$on_output" ]; then
|
||||
sed 's/^/ /' "$on_output" >&2
|
||||
else
|
||||
echo " 底层命令没有输出错误详情:$(_clashctl_real_on_target)" >&2
|
||||
fi
|
||||
rm -f "$on_output" 2>/dev/null || true
|
||||
return "$on_rc"
|
||||
fi
|
||||
else
|
||||
cat "$on_output"
|
||||
fi
|
||||
|
||||
rm -f "$on_output" 2>/dev/null || true
|
||||
|
||||
_clash_alias_after_on || {
|
||||
on_rc=$?
|
||||
echo "🔴 开启代理失败:当前 Shell 代理环境同步失败(返回码:$on_rc)" >&2
|
||||
return "$on_rc"
|
||||
}
|
||||
}
|
||||
|
||||
_clash_alias_run_off() {
|
||||
_clash_alias_unset_shell_proxy
|
||||
_clash_alias_proxy_off
|
||||
_clash_alias_set_persist_enabled "false"
|
||||
_clashctl_real off "$@" || return $?
|
||||
_clash_alias_after_off
|
||||
}
|
||||
|
||||
_clash_alias_auto_restore_proxy() {
|
||||
local proxy_file http_url https_url all_url no_proxy
|
||||
|
||||
_clash_alias_persist_enabled || return 0
|
||||
|
||||
proxy_file="${SYSTEM_PROXY_ENV_FILE:-/etc/environment}"
|
||||
[ -f "$proxy_file" ] || return 0
|
||||
grep -Fq "# >>> clash-for-linux system proxy >>>" "$proxy_file" 2>/dev/null || return 0
|
||||
|
||||
http_url="$(sed -nE 's/^http_proxy="?([^"\r\n]+)"?$/\1/p' "$proxy_file" | tail -n 1)"
|
||||
https_url="$(sed -nE 's/^https_proxy="?([^"\r\n]+)"?$/\1/p' "$proxy_file" | tail -n 1)"
|
||||
all_url="$(sed -nE 's/^all_proxy="?([^"\r\n]+)"?$/\1/p' "$proxy_file" | tail -n 1)"
|
||||
no_proxy="$(sed -nE 's/^NO_PROXY="?([^"\r\n]+)"?$/\1/p' "$proxy_file" | tail -n 1)"
|
||||
[ -n "${no_proxy:-}" ] || no_proxy="$(sed -nE 's/^no_proxy="?([^"\r\n]+)"?$/\1/p' "$proxy_file" | tail -n 1)"
|
||||
|
||||
[ -n "${http_url:-}" ] || return 0
|
||||
[ -n "${https_url:-}" ] || https_url="$http_url"
|
||||
[ -n "${all_url:-}" ] || all_url="${http_url/http:\/\//socks5://}"
|
||||
[ -n "${no_proxy:-}" ] || no_proxy="127.0.0.1,localhost,::1"
|
||||
|
||||
export http_proxy="$http_url"
|
||||
export https_proxy="$https_url"
|
||||
export HTTP_PROXY="$http_url"
|
||||
export HTTPS_PROXY="$https_url"
|
||||
export all_proxy="$all_url"
|
||||
export ALL_PROXY="$all_url"
|
||||
export no_proxy="$no_proxy"
|
||||
export NO_PROXY="$no_proxy"
|
||||
_clash_alias_export_system_proxy || return 0
|
||||
|
||||
echo "♻️ 已恢复当前 shell 代理环境(来自持久化状态)"
|
||||
return 0
|
||||
|
||||
+579
-452
File diff suppressed because it is too large
Load Diff
+44
-58
@@ -109,7 +109,7 @@ ui_download() {
|
||||
}
|
||||
|
||||
ui_target() {
|
||||
ui_color "#fd79a8" "🎯 $*"
|
||||
ui_color "#fd79a8" "🎯 $*" >&2
|
||||
}
|
||||
|
||||
ui_kv() {
|
||||
@@ -1517,10 +1517,13 @@ runtime_config_file() {
|
||||
|
||||
runtime_config_mixed_port() {
|
||||
local file
|
||||
local port
|
||||
file="$(runtime_config_file)"
|
||||
[ -s "$file" ] || return 1
|
||||
|
||||
"$(yq_bin)" eval '.["mixed-port"] // .port // ""' "$file" 2>/dev/null | head -n 1
|
||||
port="$("$(yq_bin)" eval '.["mixed-port"] // .port // ""' "$file" 2>/dev/null | head -n 1)"
|
||||
runtime_port_value_is_valid "$port" || return 1
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
runtime_config_controller_addr() {
|
||||
@@ -1533,11 +1536,33 @@ runtime_config_controller_addr() {
|
||||
|
||||
runtime_config_controller_port() {
|
||||
local addr
|
||||
local port
|
||||
addr="$(runtime_config_controller_addr 2>/dev/null || true)"
|
||||
[ -n "${addr:-}" ] || return 1
|
||||
[ "$addr" != "null" ] || return 1
|
||||
|
||||
echo "${addr##*:}"
|
||||
port="${addr##*:}"
|
||||
runtime_port_value_is_valid "$port" || return 1
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
runtime_port_value_is_valid() {
|
||||
local port="${1:-}"
|
||||
|
||||
printf '%s' "$port" | grep -Eq '^[0-9]+$' || return 1
|
||||
[ "$port" -ge 1 ] && [ "$port" -le 65535 ]
|
||||
}
|
||||
|
||||
runtime_controller_value_is_valid() {
|
||||
local controller="${1:-}"
|
||||
local port
|
||||
|
||||
[ -n "${controller:-}" ] || return 1
|
||||
[ "$controller" != "null" ] || return 1
|
||||
printf '%s' "$controller" | grep -Eq '^[^[:space:][:cntrl:]]+:[0-9]+$' || return 1
|
||||
|
||||
port="${controller##*:}"
|
||||
runtime_port_value_is_valid "$port"
|
||||
}
|
||||
|
||||
display_controller_local_addr() {
|
||||
@@ -1550,6 +1575,8 @@ display_controller_local_addr() {
|
||||
|
||||
host="${controller%:*}"
|
||||
port="${controller##*:}"
|
||||
printf '%s' "$host" | grep -Eq '^[^[:space:][:cntrl:]]+$' || return 1
|
||||
runtime_port_value_is_valid "$port" || return 1
|
||||
|
||||
if [ "$host" = "0.0.0.0" ]; then
|
||||
host="127.0.0.1"
|
||||
@@ -1568,11 +1595,14 @@ runtime_config_dns_listen() {
|
||||
|
||||
runtime_config_dns_port() {
|
||||
local listen
|
||||
local port
|
||||
listen="$(runtime_config_dns_listen 2>/dev/null || true)"
|
||||
[ -n "${listen:-}" ] || return 1
|
||||
[ "$listen" != "null" ] || return 1
|
||||
|
||||
echo "${listen##*:}"
|
||||
port="${listen##*:}"
|
||||
runtime_port_value_is_valid "$port" || return 1
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
mark_runtime_port_repair_result() {
|
||||
@@ -2533,11 +2563,15 @@ install_runtime_brief_line() {
|
||||
[ -n "${controller:-}" ] || controller="$(read_env_value "EXTERNAL_CONTROLLER" 2>/dev/null || echo "127.0.0.1:9090")"
|
||||
controller_display="$(display_controller_local_addr "$controller" 2>/dev/null || true)"
|
||||
|
||||
if ! runtime_port_value_is_valid "$mixed_port" || [ -z "${controller_display:-}" ]; then
|
||||
status_text="broken"
|
||||
fi
|
||||
|
||||
case "$status_text" in
|
||||
ready)
|
||||
echo "🟢 当前状态:ready"
|
||||
echo "🌐 本地代理:http://127.0.0.1:${mixed_port}"
|
||||
echo "🖥️ 控制台:http://${controller_display:-$controller}/ui"
|
||||
echo "💻 控制台:http://${controller_display:-$controller}/ui"
|
||||
;;
|
||||
stopped)
|
||||
echo "🔴 当前状态:stopped"
|
||||
@@ -2549,7 +2583,7 @@ install_runtime_brief_line() {
|
||||
echo "🌐 本地代理:http://127.0.0.1:${mixed_port}"
|
||||
fi
|
||||
if [ -n "${controller:-}" ]; then
|
||||
echo "🖥️ 控制台:http://${controller_display:-$controller}/ui"
|
||||
echo "💻 控制台:http://${controller_display:-$controller}/ui"
|
||||
fi
|
||||
;;
|
||||
broken)
|
||||
@@ -2562,59 +2596,11 @@ install_runtime_brief_line() {
|
||||
}
|
||||
|
||||
print_install_summary() {
|
||||
local backend mixed_port controller controller_display
|
||||
local has_subscription final_state next_action
|
||||
local clashctl_file
|
||||
|
||||
backend="$(install_plan_backend 2>/dev/null || true)"
|
||||
[ -n "${backend:-}" ] || backend="$(runtime_backend 2>/dev/null || true)"
|
||||
clashctl_file="$(clashctl_source)"
|
||||
|
||||
mixed_port="$(install_plan_mixed_port 2>/dev/null || true)"
|
||||
[ -n "${mixed_port:-}" ] || mixed_port="$(read_env_value "MIXED_PORT" 2>/dev/null || echo "7890")"
|
||||
|
||||
controller="$(install_plan_controller 2>/dev/null || true)"
|
||||
[ -n "${controller:-}" ] || controller="$(read_env_value "EXTERNAL_CONTROLLER" 2>/dev/null || echo "127.0.0.1:9090")"
|
||||
controller_display="$(display_controller_local_addr "$controller" 2>/dev/null || true)"
|
||||
|
||||
if install_has_subscription; then
|
||||
has_subscription="true"
|
||||
else
|
||||
has_subscription="false"
|
||||
if [ -f "$clashctl_file" ]; then
|
||||
CLASH_UI_BOX_ONLY=1 bash "$clashctl_file" ui || true
|
||||
fi
|
||||
|
||||
final_state="$(install_status_text)"
|
||||
next_action="$(install_default_next_action)"
|
||||
|
||||
echo
|
||||
echo "😼 安装完成"
|
||||
echo
|
||||
echo "🧩 运行后端:${backend:-unknown}"
|
||||
echo "📡 订阅:$( [ "${has_subscription:-false}" = "true" ] && echo "已配置" || echo "未配置" )"
|
||||
|
||||
case "$final_state" in
|
||||
ready)
|
||||
echo "🚦 当前状态:🟢 ready"
|
||||
echo "🌐 本地代理:http://127.0.0.1:${mixed_port}"
|
||||
echo "🖥️ 控制台:http://${controller_display:-$controller}/ui"
|
||||
;;
|
||||
stopped)
|
||||
echo "🚦 当前状态:⚪ stopped"
|
||||
;;
|
||||
verifying)
|
||||
if [ -n "${mixed_port:-}" ]; then
|
||||
echo "🌐 本地代理:http://127.0.0.1:${mixed_port}"
|
||||
fi
|
||||
if [ -n "${controller:-}" ]; then
|
||||
echo "🖥️ 控制台:http://${controller_display:-$controller}/ui"
|
||||
fi
|
||||
;;
|
||||
broken)
|
||||
echo "🚦 当前状态:🔴 broken"
|
||||
;;
|
||||
*)
|
||||
echo "🚦 当前状态:⚪ unknown"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "👉 下一步:${next_action}"
|
||||
echo
|
||||
}
|
||||
|
||||
+223
-115
@@ -438,7 +438,7 @@ normalize_runtime_config() {
|
||||
[ -s "$file" ] || die "待规范化的配置文件不存在:$file"
|
||||
|
||||
resolved_ports="$(resolve_runtime_ports)"
|
||||
eval "$resolved_ports"
|
||||
load_resolved_runtime_ports "$resolved_ports"
|
||||
|
||||
mixed_port="$MIXED_PORT_RESOLVED"
|
||||
controller="$EXTERNAL_CONTROLLER_RESOLVED"
|
||||
@@ -472,6 +472,7 @@ normalize_runtime_config() {
|
||||
|
||||
.dns.enable = (.dns.enable // true) |
|
||||
.dns["enhanced-mode"] = (.dns["enhanced-mode"] // "fake-ip") |
|
||||
.dns.ipv6 = false |
|
||||
.dns.listen = env(dns_listen_value) |
|
||||
|
||||
.proxies = (.proxies // []) |
|
||||
@@ -482,11 +483,11 @@ normalize_runtime_config() {
|
||||
|
||||
generate_secure_secret() {
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl rand -hex 24 2>/dev/null | head -n 1
|
||||
openssl rand -hex 8 2>/dev/null | head -n 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
head -c 32 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n'
|
||||
head -c 8 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n'
|
||||
}
|
||||
|
||||
is_valid_controller_secret() {
|
||||
@@ -1367,33 +1368,42 @@ csv_append_value() {
|
||||
|
||||
current_runtime_mixed_port() {
|
||||
local file="${1:-$RUNTIME_DIR/config.yaml}"
|
||||
local port
|
||||
|
||||
[ -s "$file" ] || return 1
|
||||
"$(yq_bin)" eval '.["mixed-port"] // .port // ""' "$file" 2>/dev/null | head -n 1
|
||||
port="$("$(yq_bin)" eval '.["mixed-port"] // .port // ""' "$file" 2>/dev/null | head -n 1)"
|
||||
is_valid_port_number "$port" || return 1
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
current_runtime_controller_port() {
|
||||
local file="${1:-$RUNTIME_DIR/config.yaml}"
|
||||
local addr
|
||||
local port
|
||||
|
||||
[ -s "$file" ] || return 1
|
||||
addr="$("$(yq_bin)" eval '.["external-controller"] // ""' "$file" 2>/dev/null | head -n 1)"
|
||||
[ -n "${addr:-}" ] || return 1
|
||||
[ "$addr" != "null" ] || return 1
|
||||
|
||||
echo "${addr##*:}"
|
||||
port="${addr##*:}"
|
||||
is_valid_port_number "$port" || return 1
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
current_runtime_dns_port() {
|
||||
local file="${1:-$RUNTIME_DIR/config.yaml}"
|
||||
local listen
|
||||
local port
|
||||
|
||||
[ -s "$file" ] || return 1
|
||||
listen="$("$(yq_bin)" eval '.dns.listen // ""' "$file" 2>/dev/null | head -n 1)"
|
||||
[ -n "${listen:-}" ] || return 1
|
||||
[ "$listen" != "null" ] || return 1
|
||||
|
||||
echo "${listen##*:}"
|
||||
port="${listen##*:}"
|
||||
is_valid_port_number "$port" || return 1
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
port_reserved_by_current_runtime() {
|
||||
@@ -1463,10 +1473,6 @@ resolve_runtime_port() {
|
||||
|
||||
resolved="$(resolve_free_port_excluding "$start" "$end" "$excludes_csv")"
|
||||
|
||||
if [ "$resolved" != "$preferred" ]; then
|
||||
ui_target "端口冲突:[${label}] ${preferred} 🎲 随机分配:${resolved}"
|
||||
fi
|
||||
|
||||
echo "$resolved"
|
||||
}
|
||||
|
||||
@@ -1492,6 +1498,43 @@ parse_controller_port() {
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
is_clean_controller_addr() {
|
||||
local controller="$1"
|
||||
local port
|
||||
|
||||
[ -n "${controller:-}" ] || return 1
|
||||
[ "$controller" != "null" ] || return 1
|
||||
printf '%s' "$controller" | grep -Eq '^[^[:space:][:cntrl:]]+:[0-9]+$' || return 1
|
||||
|
||||
port="${controller##*:}"
|
||||
is_valid_port_number "$port"
|
||||
}
|
||||
|
||||
load_resolved_runtime_ports() {
|
||||
local resolved="$1"
|
||||
local unexpected_lines
|
||||
local mixed controller dns
|
||||
|
||||
unexpected_lines="$(
|
||||
printf '%s\n' "$resolved" \
|
||||
| grep -Ev '^(MIXED_PORT_RESOLVED=[0-9]+|EXTERNAL_CONTROLLER_RESOLVED=[^[:space:][:cntrl:]]+:[0-9]+|CLASH_DNS_PORT_RESOLVED=[0-9]+)$' \
|
||||
| grep -v '^$' || true
|
||||
)"
|
||||
[ -z "${unexpected_lines:-}" ] || die "runtime port resolution output is polluted"
|
||||
|
||||
mixed="$(printf '%s\n' "$resolved" | sed -n 's/^MIXED_PORT_RESOLVED=//p' | head -n 1)"
|
||||
controller="$(printf '%s\n' "$resolved" | sed -n 's/^EXTERNAL_CONTROLLER_RESOLVED=//p' | head -n 1)"
|
||||
dns="$(printf '%s\n' "$resolved" | sed -n 's/^CLASH_DNS_PORT_RESOLVED=//p' | head -n 1)"
|
||||
|
||||
is_valid_port_number "$mixed" || die "invalid MIXED_PORT_RESOLVED: $mixed"
|
||||
is_clean_controller_addr "$controller" || die "invalid EXTERNAL_CONTROLLER_RESOLVED: $controller"
|
||||
is_valid_port_number "$dns" || die "invalid CLASH_DNS_PORT_RESOLVED: $dns"
|
||||
|
||||
MIXED_PORT_RESOLVED="$mixed"
|
||||
EXTERNAL_CONTROLLER_RESOLVED="$controller"
|
||||
CLASH_DNS_PORT_RESOLVED="$dns"
|
||||
}
|
||||
|
||||
resolve_runtime_ports() {
|
||||
local preferred_mixed preferred_controller preferred_dns
|
||||
local controller_host preferred_controller_port
|
||||
@@ -1509,12 +1552,15 @@ resolve_runtime_ports() {
|
||||
preferred_controller_port="$(parse_controller_port "$preferred_controller")"
|
||||
|
||||
mixed_port="$(resolve_runtime_port "$preferred_mixed" 7890 7999 "$used_ports" "mixed-port")"
|
||||
is_valid_port_number "$mixed_port" || die "invalid mixed-port resolution: $mixed_port"
|
||||
used_ports="$(csv_append_value "$used_ports" "$mixed_port")"
|
||||
|
||||
controller_port="$(resolve_runtime_port "$preferred_controller_port" 9090 9199 "$used_ports" "external-controller")"
|
||||
is_valid_port_number "$controller_port" || die "invalid external-controller resolution: $controller_port"
|
||||
used_ports="$(csv_append_value "$used_ports" "$controller_port")"
|
||||
|
||||
dns_port="$(resolve_runtime_port "$preferred_dns" 1053 1199 "$used_ports" "dns.listen")"
|
||||
is_valid_port_number "$dns_port" || die "invalid dns.listen resolution: $dns_port"
|
||||
used_ports="$(csv_append_value "$used_ports" "$dns_port")"
|
||||
|
||||
printf 'MIXED_PORT_RESOLVED=%s\n' "$mixed_port"
|
||||
@@ -1528,7 +1574,7 @@ mark_install_port_plan() {
|
||||
local changed_mixed changed_controller changed_dns
|
||||
|
||||
resolved="$(resolve_runtime_ports)"
|
||||
eval "$resolved"
|
||||
load_resolved_runtime_ports "$resolved"
|
||||
|
||||
preferred_mixed="${MIXED_PORT:-7890}"
|
||||
preferred_controller="${EXTERNAL_CONTROLLER:-0.0.0.0:9090}"
|
||||
@@ -1661,13 +1707,14 @@ show_subscription() {
|
||||
|
||||
bootstrap_subscription_from_install_input() {
|
||||
local url="$1"
|
||||
local fmt="${2:-convert}"
|
||||
local fmt="${2:-}"
|
||||
local name="${3:-default}"
|
||||
local file
|
||||
|
||||
[ -n "${url:-}" ] || return 1
|
||||
|
||||
subscription_url_is_supported "$url" || die "订阅地址格式不合法"
|
||||
[ -n "${fmt:-}" ] || fmt="$(detect_subscription_format "$url")"
|
||||
|
||||
case "$fmt" in
|
||||
clash|convert) ;;
|
||||
@@ -1733,32 +1780,54 @@ subscription_name_has_url() {
|
||||
}
|
||||
|
||||
ensure_subscription_bootstrap_for_install() {
|
||||
local env_url active_name
|
||||
local env_url active_name input_fmt current_url current_fmt
|
||||
|
||||
env_url="${CLASH_SUBSCRIPTION_URL:-}"
|
||||
active_name="${1:-default}"
|
||||
|
||||
[ -n "${env_url:-}" ] || return 0
|
||||
|
||||
ensure_subscriptions_file
|
||||
|
||||
# 只在“当前完全没有订阅”时,才允许用 .env 初始化
|
||||
# Only initialize from .env when there is no subscription yet.
|
||||
if subscriptions_has_any_usable_source; then
|
||||
current_url="$(subscription_url_by_name "$active_name" 2>/dev/null || true)"
|
||||
current_fmt="$(subscription_format_by_name "$active_name" 2>/dev/null || true)"
|
||||
|
||||
if [ -n "${current_url:-}" ]; then
|
||||
input_fmt="$(detect_subscription_format "$current_url")"
|
||||
if [ "$current_fmt" = "convert" ] && [ "$input_fmt" = "clash" ]; then
|
||||
info "安装订阅格式判定:$active_name -> $input_fmt"
|
||||
bootstrap_subscription_from_install_input "$current_url" "$input_fmt" "$active_name"
|
||||
fi
|
||||
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${env_url:-}" ]; then
|
||||
input_fmt="$(detect_subscription_format "$env_url")"
|
||||
info "安装订阅格式判定:$active_name -> $input_fmt"
|
||||
bootstrap_subscription_from_install_input "$env_url" "$input_fmt" "$active_name"
|
||||
fi
|
||||
|
||||
return 0
|
||||
fi
|
||||
|
||||
bootstrap_subscription_from_install_input "$env_url" "convert" "$active_name"
|
||||
[ -n "${env_url:-}" ] || return 0
|
||||
|
||||
input_fmt="$(detect_subscription_format "$env_url")"
|
||||
info "安装订阅格式判定:$active_name -> $input_fmt"
|
||||
bootstrap_subscription_from_install_input "$env_url" "$input_fmt" "$active_name"
|
||||
}
|
||||
|
||||
set_subscription() {
|
||||
local url="$1"
|
||||
local fmt="${2:-convert}"
|
||||
local fmt="${2:-}"
|
||||
local name="${3:-default}"
|
||||
local file
|
||||
|
||||
[ -n "$url" ] || die "订阅地址不能为空"
|
||||
|
||||
subscription_url_is_supported "$url" || die "订阅地址格式不合法"
|
||||
[ -n "${fmt:-}" ] || fmt="$(detect_subscription_format "$url")"
|
||||
|
||||
case "$fmt" in
|
||||
clash|convert) ;;
|
||||
@@ -2219,33 +2288,44 @@ calculate_runtime_risk_level() {
|
||||
echo "low"
|
||||
}
|
||||
|
||||
print_subscription_table_header() {
|
||||
local show_index="${1:-false}"
|
||||
|
||||
if [ "$show_index" = "true" ]; then
|
||||
echo " 编号 名称 类型 URL"
|
||||
else
|
||||
echo " 名称 类型 URL"
|
||||
fi
|
||||
}
|
||||
|
||||
print_subscription_pick_line() {
|
||||
local index="$1"
|
||||
local name="$2"
|
||||
local active enabled_text health_text fail_count type_text marker
|
||||
local _url_mode="${3:-full}"
|
||||
local show_index="${4:-true}"
|
||||
local active type_text marker url_text
|
||||
|
||||
[ -n "${name:-}" ] || return 0
|
||||
|
||||
active="$(active_subscription_name 2>/dev/null || true)"
|
||||
|
||||
if [ "$name" = "$active" ]; then
|
||||
marker="[当前]"
|
||||
marker="$(printf '\033[32m*\033[0m')"
|
||||
else
|
||||
marker=""
|
||||
marker=" "
|
||||
fi
|
||||
|
||||
if subscription_enabled "$name"; then
|
||||
enabled_text="enabled"
|
||||
else
|
||||
enabled_text="disabled"
|
||||
fi
|
||||
|
||||
health_text="$(subscription_health_status "$name" 2>/dev/null || echo "unknown")"
|
||||
fail_count="$(subscription_fail_count "$name" 2>/dev/null || echo "0")"
|
||||
type_text="$(subscription_format_by_name "$name" 2>/dev/null || echo "clash")"
|
||||
url_text="$(subscription_url_by_name "$name" 2>/dev/null || true)"
|
||||
[ -n "${url_text:-}" ] || url_text="-"
|
||||
|
||||
printf ' %-2s) %-16s %-8s %-9s %-8s fail=%-3s %s\n' \
|
||||
"$index" "$name" "$type_text" "$enabled_text" "$health_text" "$fail_count" "$marker"
|
||||
if [ "$show_index" = "true" ]; then
|
||||
printf '%b %-2s) %-16s %-8s %s\n' \
|
||||
"$marker" "$index" "$name" "$type_text" "$url_text"
|
||||
else
|
||||
printf '%b %-16s %-8s %s\n' \
|
||||
"$marker" "$name" "$type_text" "$url_text"
|
||||
fi
|
||||
}
|
||||
|
||||
print_subscription_summary_line() {
|
||||
@@ -2287,16 +2367,17 @@ print_subscription_summary_line() {
|
||||
}
|
||||
|
||||
list_subscriptions() {
|
||||
local file name found="false"
|
||||
local file name found="false" idx=1
|
||||
|
||||
file="$(subscriptions_file)"
|
||||
ensure_subscriptions_file
|
||||
|
||||
echo " 名称 类型 启用状态 健康状态 失败次数"
|
||||
print_subscription_table_header "false"
|
||||
while IFS= read -r name; do
|
||||
[ -n "${name:-}" ] || continue
|
||||
found="true"
|
||||
print_subscription_summary_line "$name"
|
||||
print_subscription_pick_line "$idx" "$name" "full" "false"
|
||||
idx=$((idx + 1))
|
||||
done < <("$(yq_bin)" eval '.sources | keys | .[]' "$file" 2>/dev/null)
|
||||
|
||||
[ "$found" = "true" ] || echo " 暂无订阅"
|
||||
@@ -2312,49 +2393,6 @@ disable_subscription() {
|
||||
set_subscription_enabled "$name" "false"
|
||||
}
|
||||
|
||||
list_subscriptions_verbose() {
|
||||
local file active name enabled fmt url
|
||||
|
||||
file="$(subscriptions_file)"
|
||||
ensure_subscriptions_file
|
||||
|
||||
active="$(active_subscription_name)"
|
||||
|
||||
while IFS= read -r name; do
|
||||
[ -n "${name:-}" ] || continue
|
||||
|
||||
enabled="$("$(yq_bin)" eval ".sources.${name}.enabled // false" "$file" 2>/dev/null)"
|
||||
fmt="$("$(yq_bin)" eval ".sources.${name}.type // \"clash\"" "$file" 2>/dev/null)"
|
||||
url="$("$(yq_bin)" eval ".sources.${name}.url // \"\"" "$file" 2>/dev/null)"
|
||||
|
||||
if [ "$name" = "$active" ]; then
|
||||
echo "* $name"
|
||||
else
|
||||
echo " $name"
|
||||
fi
|
||||
|
||||
echo " enabled : $enabled"
|
||||
echo " type : $fmt"
|
||||
echo " health : $(subscription_health_status "$name" 2>/dev/null || echo "unknown")"
|
||||
echo " fail_count : $(subscription_fail_count "$name" 2>/dev/null || echo "0")"
|
||||
echo " auto_disabled : $(if subscription_auto_disabled "$name"; then echo true; else echo false; fi)"
|
||||
|
||||
if [ -n "$(subscription_last_success "$name" 2>/dev/null || true)" ]; then
|
||||
echo " last_success : $(subscription_last_success "$name" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [ -n "$(subscription_last_failure "$name" 2>/dev/null || true)" ]; then
|
||||
echo " last_failure : $(subscription_last_failure "$name" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [ -n "$(subscription_last_error "$name" 2>/dev/null || true)" ]; then
|
||||
echo " last_error : $(subscription_last_error "$name" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
[ -n "${url:-}" ] && echo " url : $url"
|
||||
done < <("$(yq_bin)" eval '.sources | keys | .[]' "$file")
|
||||
}
|
||||
|
||||
subscription_list_overview_lines() {
|
||||
local active recommended
|
||||
|
||||
@@ -2379,28 +2417,7 @@ subscription_list_overview_lines() {
|
||||
}
|
||||
|
||||
subscription_list_recommendation_lines() {
|
||||
local active recommended
|
||||
|
||||
active="$(active_subscription_name 2>/dev/null || true)"
|
||||
recommended="$(recommended_subscription_name 2>/dev/null || true)"
|
||||
|
||||
if [ -n "${active:-}" ] && ! active_subscription_enabled 2>/dev/null; then
|
||||
echo "👉 clashctl use"
|
||||
echo "👉 clashctl health"
|
||||
echo "👉 clashctl ls --verbose"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${recommended:-}" ] && [ "${recommended:-}" != "${active:-}" ]; then
|
||||
echo "👉 clashctl use ${recommended}"
|
||||
echo "👉 clashctl health ${recommended}"
|
||||
echo "👉 clashctl ls --verbose"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "👉 clashctl use"
|
||||
echo "👉 clashctl health"
|
||||
echo "👉 clashctl ls --verbose"
|
||||
echo "👉 clashctl use 切换当前使用的订阅"
|
||||
}
|
||||
|
||||
detect_subscription_format() {
|
||||
@@ -2494,28 +2511,70 @@ subconverter_running() {
|
||||
}
|
||||
|
||||
start_subconverter() {
|
||||
local home bin log_file
|
||||
local home bin log_file pid_file pid i old_pwd exit_status
|
||||
|
||||
home="$(subconverter_home)"
|
||||
bin="$(subconverter_bin)"
|
||||
log_file="$(subconverter_log_file)"
|
||||
pid_file="$(subconverter_pid_file)"
|
||||
exit_status="unknown"
|
||||
|
||||
[ -x "$bin" ] || die "subconverter 未安装:$bin"
|
||||
[ -f "$bin" ] || die "subconverter 未安装:$bin"
|
||||
if [ ! -x "$bin" ]; then
|
||||
chmod +x "$bin" 2>/dev/null || die "subconverter 无法修正执行权限:$bin"
|
||||
fi
|
||||
[ -x "$bin" ] || die "subconverter 文件不可执行:$bin"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
if subconverter_running; then
|
||||
return 0
|
||||
if is_port_in_use "$(subconverter_port)"; then
|
||||
return 0
|
||||
fi
|
||||
warn "检测到旧 subconverter 进程存在但端口未监听,正在重启"
|
||||
stop_subconverter || true
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$home"
|
||||
nohup "$bin" > "$log_file" 2>&1 &
|
||||
echo $! > "$(subconverter_pid_file)"
|
||||
)
|
||||
old_pwd="$(pwd)"
|
||||
rm -f "$pid_file" 2>/dev/null || true
|
||||
cd "$home" || die "subconverter 工作目录不可用:$home"
|
||||
nohup "$bin" > "$log_file" 2>&1 &
|
||||
pid=$!
|
||||
cd "$old_pwd" || true
|
||||
echo "$pid" > "$pid_file"
|
||||
|
||||
sleep 1
|
||||
for i in 1 2 3 4 5; do
|
||||
if subconverter_running && is_port_in_use "$(subconverter_port)"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
subconverter_running || die "subconverter 启动失败"
|
||||
pid="$(cat "$pid_file" 2>/dev/null || true)"
|
||||
if [ -n "${pid:-}" ] && ! kill -0 "$pid" 2>/dev/null; then
|
||||
if wait "$pid" 2>/dev/null; then
|
||||
exit_status="0"
|
||||
else
|
||||
exit_status="$?"
|
||||
fi
|
||||
fi
|
||||
{
|
||||
error "subconverter 启动失败"
|
||||
warn "启动命令:cd \"$home\" && \"$bin\""
|
||||
warn "监听端口:$(subconverter_port)"
|
||||
warn "pid 文件:$pid_file"
|
||||
warn "pid:${pid:-unknown}"
|
||||
if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
warn "进程状态:仍在运行,但未监听预期端口"
|
||||
else
|
||||
warn "进程状态:已退出或未成功启动"
|
||||
warn "退出状态:$exit_status"
|
||||
fi
|
||||
warn "日志文件:$log_file"
|
||||
if [ -f "$log_file" ]; then
|
||||
tail -n 20 "$log_file" 2>/dev/null | sed 's/^/ /' >&2 || true
|
||||
fi
|
||||
} >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
stop_subconverter() {
|
||||
@@ -2702,7 +2761,9 @@ convert_subscription_via_subconverter() {
|
||||
local url="$1"
|
||||
local out_file="$2"
|
||||
local fetch_reason="${3:-auto}"
|
||||
local api tmp_file
|
||||
local convert_reason="${4:-subscription-type-convert}"
|
||||
local api tmp_file curl_error_file
|
||||
local curl_meta curl_rc http_code effective_url errexit_was_set
|
||||
local log_file
|
||||
local validate_ok="false"
|
||||
|
||||
@@ -2735,18 +2796,65 @@ convert_subscription_via_subconverter() {
|
||||
api="$(subconverter_url)/sub"
|
||||
log_file="$(subconverter_log_file)"
|
||||
tmp_file="$(mktemp)"
|
||||
curl_error_file="$(mktemp)"
|
||||
rm -f "$tmp_file" 2>/dev/null || true
|
||||
|
||||
info "正在通过 subconverter 转换订阅"
|
||||
case "$convert_reason" in
|
||||
direct-clash-invalid)
|
||||
info "转换原因:直连订阅已下载,但不是可直接运行的 Clash YAML"
|
||||
;;
|
||||
subscription-type-convert)
|
||||
info "转换原因:订阅类型为 convert"
|
||||
;;
|
||||
*)
|
||||
info "转换原因:$convert_reason"
|
||||
;;
|
||||
esac
|
||||
info "subconverter 请求:GET $api"
|
||||
info "subconverter 参数:target=clash"
|
||||
info "subconverter 参数:url=$url"
|
||||
info "subconverter 未发送参数:insert/config/emoji/list(使用 subconverter 默认值)"
|
||||
|
||||
if ! curl -fL -G "$api" \
|
||||
errexit_was_set="false"
|
||||
case "$-" in
|
||||
*e*)
|
||||
errexit_was_set="true"
|
||||
set +e
|
||||
;;
|
||||
esac
|
||||
|
||||
curl_meta="$(curl -sS -L -G "$api" \
|
||||
--data-urlencode "target=clash" \
|
||||
--data-urlencode "url=$url" \
|
||||
-o "$tmp_file"; then
|
||||
rm -f "$tmp_file" 2>/dev/null || true
|
||||
-o "$tmp_file" \
|
||||
-w '%{http_code}\n%{url_effective}' 2>"$curl_error_file")"
|
||||
curl_rc=$?
|
||||
|
||||
[ "$errexit_was_set" = "true" ] && set -e
|
||||
|
||||
http_code="$(printf '%s\n' "$curl_meta" | head -n 1)"
|
||||
effective_url="$(printf '%s\n' "$curl_meta" | sed -n '2p')"
|
||||
|
||||
[ -n "${effective_url:-}" ] && info "subconverter 实际请求 URL:$effective_url"
|
||||
[ -n "${http_code:-}" ] && info "subconverter HTTP 状态码:$http_code"
|
||||
|
||||
if [ "$curl_rc" -ne 0 ] || [ -z "${http_code:-}" ] || [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
|
||||
warn "subconverter 转换请求失败:curl_rc=$curl_rc http_code=${http_code:-unknown}"
|
||||
[ -s "$curl_error_file" ] && {
|
||||
warn "curl 错误输出:"
|
||||
head -n 20 "$curl_error_file" >&2
|
||||
}
|
||||
[ -s "$tmp_file" ] && {
|
||||
warn "subconverter 响应体预览:"
|
||||
head -n 20 "$tmp_file" >&2
|
||||
}
|
||||
rm -f "$tmp_file" "$curl_error_file" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "$curl_error_file" 2>/dev/null || true
|
||||
|
||||
[ -s "$tmp_file" ] || {
|
||||
rm -f "$tmp_file" 2>/dev/null || true
|
||||
return 1
|
||||
@@ -3004,7 +3112,7 @@ fetch_subscription_source() {
|
||||
candidate_file="$(mktemp)"
|
||||
rm -f "$candidate_file" 2>/dev/null || true
|
||||
|
||||
if convert_subscription_via_subconverter "$url" "$raw_file" "$fetch_reason"; then
|
||||
if convert_subscription_via_subconverter "$url" "$raw_file" "$fetch_reason" "direct-clash-invalid"; then
|
||||
if build_runtime_candidate_from_payload "$raw_file" "$candidate_file"; then
|
||||
mv -f "$candidate_file" "$out_file"
|
||||
rm -f "$raw_file" 2>/dev/null || true
|
||||
@@ -3028,7 +3136,7 @@ fetch_subscription_source() {
|
||||
;;
|
||||
esac
|
||||
|
||||
if convert_subscription_via_subconverter "$url" "$raw_file" "$fetch_reason"; then
|
||||
if convert_subscription_via_subconverter "$url" "$raw_file" "$fetch_reason" "subscription-type-convert"; then
|
||||
if build_runtime_candidate_from_payload "$raw_file" "$candidate_file"; then
|
||||
mv -f "$candidate_file" "$out_file"
|
||||
rm -f "$raw_file" 2>/dev/null || true
|
||||
|
||||
@@ -176,12 +176,20 @@ controller_secret() {
|
||||
}
|
||||
|
||||
controller_api_base() {
|
||||
local addr
|
||||
local addr host port
|
||||
|
||||
addr="$(controller_addr)"
|
||||
[ -n "${addr:-}" ] || die "未找到 external-controller"
|
||||
[ "$addr" != "null" ] || die "未找到 external-controller"
|
||||
|
||||
host="${addr%:*}"
|
||||
port="${addr##*:}"
|
||||
case "$host" in
|
||||
0.0.0.0)
|
||||
addr="127.0.0.1:${port}"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "http://$addr"
|
||||
}
|
||||
|
||||
|
||||
+214
-27
@@ -166,16 +166,68 @@ mark_subconverter_version_installed() {
|
||||
write_runtime_value "SUBCONVERTER_VERSION_INSTALLED" "$version"
|
||||
}
|
||||
|
||||
detect_subconverter_package_type() {
|
||||
local file="$1"
|
||||
local desc magic
|
||||
|
||||
if tar -tzf "$file" >/dev/null 2>&1; then
|
||||
echo "tar.gz"
|
||||
return 0
|
||||
fi
|
||||
|
||||
magic="$(od -An -tx1 -N4 "$file" 2>/dev/null | tr -d '[:space:]' || true)"
|
||||
|
||||
case "$magic" in
|
||||
504b0304|504b0506|504b0708)
|
||||
echo "zip"
|
||||
return 0
|
||||
;;
|
||||
7f454c46)
|
||||
echo "binary"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v unzip >/dev/null 2>&1 && unzip -tq "$file" >/dev/null 2>&1; then
|
||||
echo "zip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
desc="$(file -b "$file" 2>/dev/null || true)"
|
||||
case "$desc" in
|
||||
*ELF*|*executable*)
|
||||
echo "binary"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "unknown"
|
||||
}
|
||||
|
||||
find_subconverter_binary() {
|
||||
local search_dir="$1"
|
||||
local found
|
||||
|
||||
found="$(find "$search_dir" \( -type f -o -type l \) -name subconverter -print 2>/dev/null | head -n 1)"
|
||||
[ -n "${found:-}" ] || return 1
|
||||
echo "$found"
|
||||
}
|
||||
|
||||
resolve_subconverter() {
|
||||
local arch version file url tmp_dir tmp_file target_dir installed_version
|
||||
local extract_dir package_type found_bin target_bin
|
||||
|
||||
arch="$(get_arch)"
|
||||
version="${SUBCONVERTER_VERSION:-$DEFAULT_SUBCONVERTER_VERSION}"
|
||||
target_dir="$(subconverter_home)"
|
||||
target_bin="$(subconverter_bin)"
|
||||
installed_version="$(subconverter_version_file_value)"
|
||||
|
||||
if [ -x "$(subconverter_bin)" ] && [ "$installed_version" = "$version" ]; then
|
||||
return 0
|
||||
if [ -f "$target_bin" ] && [ "$installed_version" = "$version" ]; then
|
||||
chmod +x "$target_bin" 2>/dev/null || true
|
||||
if [ -x "$target_bin" ]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$arch" in
|
||||
@@ -188,20 +240,56 @@ resolve_subconverter() {
|
||||
url="https://github.com/tindy2013/subconverter/releases/download/${version}/${file}"
|
||||
tmp_dir="$(mktemp -d)"
|
||||
tmp_file="$tmp_dir/$file"
|
||||
extract_dir="$tmp_dir/extract"
|
||||
|
||||
download_file "$url" "$tmp_file" "subconverter"
|
||||
info "subconverter 下载文件:$tmp_file"
|
||||
|
||||
package_type="$(detect_subconverter_package_type "$tmp_file")"
|
||||
info "subconverter 下载文件类型:$package_type"
|
||||
|
||||
rm -rf "$target_dir"
|
||||
mkdir -p "$target_dir"
|
||||
mkdir -p "$target_dir" "$extract_dir"
|
||||
|
||||
tar -xzf "$tmp_file" -C "$target_dir"
|
||||
case "$package_type" in
|
||||
tar.gz)
|
||||
info "subconverter 解压目录:$extract_dir"
|
||||
tar -xzf "$tmp_file" -C "$extract_dir"
|
||||
found_bin="$(find_subconverter_binary "$extract_dir" 2>/dev/null || true)"
|
||||
;;
|
||||
zip)
|
||||
command -v unzip >/dev/null 2>&1 || die "解压 subconverter zip 失败:系统缺少 unzip"
|
||||
info "subconverter 解压目录:$extract_dir"
|
||||
unzip -oq "$tmp_file" -d "$extract_dir"
|
||||
found_bin="$(find_subconverter_binary "$extract_dir" 2>/dev/null || true)"
|
||||
;;
|
||||
binary)
|
||||
info "subconverter 解压目录:无需解压"
|
||||
found_bin="$tmp_file"
|
||||
;;
|
||||
*)
|
||||
if command -v file >/dev/null 2>&1; then
|
||||
file "$tmp_file" >&2 || true
|
||||
fi
|
||||
die "subconverter 下载内容异常:无法识别下载包类型(文件:$tmp_file)"
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -x "$(subconverter_bin)" ] || {
|
||||
rm -rf "$tmp_dir"
|
||||
die "解压后未找到 subconverter 可执行文件"
|
||||
[ -n "${found_bin:-}" ] && [ -f "$found_bin" ] || {
|
||||
find "$extract_dir" -maxdepth 3 \( -type f -o -type l \) 2>/dev/null | sed 's/^/ /' >&2 || true
|
||||
die "解压后未找到 subconverter 文件"
|
||||
}
|
||||
|
||||
info "subconverter 实际文件:$found_bin"
|
||||
cp -f "$found_bin" "$target_bin"
|
||||
chmod +x "$target_bin"
|
||||
info "subconverter 最终落盘:$target_bin"
|
||||
|
||||
[ -x "$target_bin" ] || {
|
||||
rm -rf "$tmp_dir"
|
||||
die "subconverter 文件不可执行:$target_bin"
|
||||
}
|
||||
|
||||
chmod +x "$(subconverter_bin)"
|
||||
mark_subconverter_version_installed "$version"
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
@@ -241,8 +329,85 @@ ensure_runtime_config_ready() {
|
||||
die "最后成功配置也不可用:$last_file"
|
||||
}
|
||||
|
||||
tun_public_ip_without_proxy_env() {
|
||||
local ip route_dev
|
||||
|
||||
route_dev="$(default_route_dev 2>/dev/null || true)"
|
||||
if [ -n "${route_dev:-}" ] && ! printf '%s\n' "$route_dev" | grep -Eiq '(tun|utun|mihomo|clash|meta)'; then
|
||||
ip="$(
|
||||
env \
|
||||
-u http_proxy \
|
||||
-u https_proxy \
|
||||
-u HTTP_PROXY \
|
||||
-u HTTPS_PROXY \
|
||||
-u all_proxy \
|
||||
-u ALL_PROXY \
|
||||
curl --interface "$route_dev" -fsSL --connect-timeout 3 --max-time 6 https://ip.sb 2>/dev/null \
|
||||
| head -n 1 \
|
||||
| tr -d '\r' \
|
||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
)"
|
||||
|
||||
if [ -n "${ip:-}" ]; then
|
||||
echo "$ip"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
env \
|
||||
-u http_proxy \
|
||||
-u https_proxy \
|
||||
-u HTTP_PROXY \
|
||||
-u HTTPS_PROXY \
|
||||
-u all_proxy \
|
||||
-u ALL_PROXY \
|
||||
curl -fsSL --connect-timeout 3 --max-time 6 https://ip.sb 2>/dev/null \
|
||||
| head -n 1 \
|
||||
| tr -d '\r' \
|
||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
}
|
||||
|
||||
tun_public_ip_with_current_route() {
|
||||
env \
|
||||
-u http_proxy \
|
||||
-u https_proxy \
|
||||
-u HTTP_PROXY \
|
||||
-u HTTPS_PROXY \
|
||||
-u all_proxy \
|
||||
-u ALL_PROXY \
|
||||
curl -fsSL --connect-timeout 3 --max-time 6 https://ip.sb 2>/dev/null \
|
||||
| head -n 1 \
|
||||
| tr -d '\r' \
|
||||
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
}
|
||||
|
||||
tun_traffic_effective_check() {
|
||||
local host_ip current_ip
|
||||
|
||||
host_ip="$(tun_public_ip_without_proxy_env 2>/dev/null || true)"
|
||||
current_ip="$(tun_public_ip_with_current_route 2>/dev/null || true)"
|
||||
|
||||
if [ -z "${host_ip:-}" ]; then
|
||||
echo "host-ip-unavailable"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${current_ip:-}" ]; then
|
||||
echo "current-ip-unavailable"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$current_ip" != "$host_ip" ]; then
|
||||
echo "ok"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "traffic-same-as-host"
|
||||
return 1
|
||||
}
|
||||
|
||||
tun_effective_check() {
|
||||
local config_tun_enabled auto_route controller_ok route_ok
|
||||
local config_tun_enabled controller_ok traffic_result
|
||||
|
||||
config_tun_enabled="$(runtime_config_tun_enabled 2>/dev/null || true)"
|
||||
|
||||
@@ -271,24 +436,16 @@ tun_effective_check() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
auto_route="$(runtime_config_tun_auto_route 2>/dev/null || true)"
|
||||
route_ok="unknown"
|
||||
traffic_result="$(tun_traffic_effective_check 2>/dev/null || true)"
|
||||
[ -n "${traffic_result:-}" ] || traffic_result="traffic-unknown"
|
||||
|
||||
if [ "${auto_route:-false}" = "true" ]; then
|
||||
if default_route_is_tun_like; then
|
||||
route_ok="true"
|
||||
else
|
||||
route_ok="false"
|
||||
fi
|
||||
|
||||
if [ "$route_ok" != "true" ]; then
|
||||
echo "default-route-not-tun"
|
||||
return 1
|
||||
fi
|
||||
if [ "$traffic_result" = "ok" ]; then
|
||||
echo "ok"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "ok"
|
||||
return 0
|
||||
echo "$traffic_result"
|
||||
return 1
|
||||
}
|
||||
|
||||
tun_disable_check() {
|
||||
@@ -317,6 +474,22 @@ tun_disable_check() {
|
||||
return 0
|
||||
}
|
||||
|
||||
wait_runtime_controller_ready() {
|
||||
local max_try="${1:-8}"
|
||||
local i=0
|
||||
|
||||
while [ "$i" -lt "$max_try" ]; do
|
||||
if proxy_controller_reachable 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
start_runtime() {
|
||||
local config_file="$RUNTIME_DIR/config.yaml"
|
||||
|
||||
@@ -327,16 +500,30 @@ start_runtime() {
|
||||
local old_pid
|
||||
old_pid="$(cat "$RUNTIME_DIR/mihomo.pid" 2>/dev/null || true)"
|
||||
if [ -n "${old_pid:-}" ] && kill -0 "$old_pid" 2>/dev/null; then
|
||||
warn "Mihomo 已在运行:pid=$old_pid"
|
||||
return 0
|
||||
if proxy_controller_reachable 2>/dev/null; then
|
||||
warn "Mihomo 已在运行:pid=$old_pid"
|
||||
return 0
|
||||
fi
|
||||
|
||||
warn "Mihomo 进程仍在运行但控制器不可访问,正在重启以加载当前配置"
|
||||
stop_runtime || true
|
||||
else
|
||||
rm -f "$RUNTIME_DIR/mihomo.pid"
|
||||
fi
|
||||
rm -f "$RUNTIME_DIR/mihomo.pid"
|
||||
fi
|
||||
|
||||
nohup "$(runtime_kernel_bin)" -f "$config_file" -d "$RUNTIME_DIR" \
|
||||
> "$LOG_DIR/mihomo.out.log" 2>&1 &
|
||||
|
||||
echo $! > "$RUNTIME_DIR/mihomo.pid"
|
||||
|
||||
if ! wait_runtime_controller_ready 8; then
|
||||
warn "$(runtime_kernel_name) 已启动,但控制器未在预期时间内可访问"
|
||||
local new_pid
|
||||
new_pid="$(cat "$RUNTIME_DIR/mihomo.pid" 2>/dev/null || true)"
|
||||
[ -n "${new_pid:-}" ] && kill -0 "$new_pid" 2>/dev/null || return 1
|
||||
fi
|
||||
|
||||
success "$(runtime_kernel_name) 已启动:pid=$(cat "$RUNTIME_DIR/mihomo.pid")"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user