From 12b465c02ec940d05b0dc82b48e91b7942405f60 Mon Sep 17 00:00:00 2001 From: Arvin <62139570+wnlen@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:31:49 +0800 Subject: [PATCH] Fix clashoff stopping runtime --- scripts/core/clashctl.sh | 51 +++++++- scripts/dev/check-off-stops-runtime.sh | 170 +++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 scripts/dev/check-off-stops-runtime.sh diff --git a/scripts/core/clashctl.sh b/scripts/core/clashctl.sh index 7d1834d..f38f1a6 100644 --- a/scripts/core/clashctl.sh +++ b/scripts/core/clashctl.sh @@ -296,27 +296,64 @@ cmd_on() { } cmd_off() { - local system_proxy_rc + local system_proxy_rc stop_rc + local system_proxy_state_before system_proxy_cleanup_blocked="false" prepare + system_proxy_state_before="$(system_proxy_status 2>/dev/null || echo off)" + if system_proxy_disable; then : else system_proxy_rc=$? write_runtime_value "RUNTIME_BOOT_PROXY_KEEP" "false" 2>/dev/null || true - if [ "$system_proxy_rc" -eq 2 ]; then - ui_warn "当前环境不支持清理系统代理持久块,已继续关闭运行时" - else - ui_warn "系统代理持久块清理失败,已继续关闭运行时" - ui_next "clashctl doctor" + + if [ "$system_proxy_state_before" = "on" ]; then + system_proxy_cleanup_blocked="true" + if [ "$system_proxy_rc" -eq 2 ]; then + ui_warn "当前环境不支持清理系统代理持久块,已继续关闭运行时" + else + ui_warn "系统代理持久块清理失败,已继续关闭运行时" + ui_next "clashctl doctor" + fi fi fi + + if status_is_running 2>/dev/null; then + stop_rc=0 + service_stop || stop_rc=$? + if [ "$stop_rc" -ne 0 ]; then + die_state "关闭运行时失败:运行后端 stop 返回 ${stop_rc}" "clashctl logs" + fi + + if ! wait_runtime_stopped 8; then + die_state "关闭运行时失败:代理内核仍在运行" "clashctl logs" + fi + fi + + if [ "$system_proxy_cleanup_blocked" = "true" ]; then + die_state "系统代理持久块未清理:$(system_proxy_env_file)" "请使用有权限的用户执行 clashctl off,或手动清理该文件中的 clash-for-linux 代理块" + fi + ui_blank - echo "🧹 系统代理已关闭" + echo "🧹 代理已关闭" ui_blank } +wait_runtime_stopped() { + local timeout="${1:-8}" + local elapsed=0 + + while status_is_running 2>/dev/null; do + [ "$elapsed" -lt "$timeout" ] || return 1 + sleep 1 + elapsed=$((elapsed + 1)) + done + + return 0 +} + ui_internal_url() { local controller host port controller="${1:-}" diff --git a/scripts/dev/check-off-stops-runtime.sh b/scripts/dev/check-off-stops-runtime.sh new file mode 100644 index 0000000..d77cd3a --- /dev/null +++ b/scripts/dev/check-off-stops-runtime.sh @@ -0,0 +1,170 @@ +#!/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 functions; suppress the no-arg usage printed by the command dispatcher. + source "$PROJECT_DIR/scripts/core/clashctl.sh" >/dev/null +} + +source_clashctl_for_tests + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +prepare() { :; } +ui_warn() { printf 'warn: %s\n' "$*" >> "$tmp_dir/output"; } +ui_next() { printf 'next: %s\n' "$*" >> "$tmp_dir/output"; } +ui_blank() { :; } +system_proxy_env_file() { printf '%s\n' "$tmp_dir/environment"; } +write_runtime_value() { :; } +die_state() { + printf 'die: %s\nnext: %s\n' "$1" "$2" >> "$tmp_dir/output" + exit 1 +} + +run_case() { + local name="$1" + local proxy_rc="$2" + local stop_calls_file="$tmp_dir/stop-calls" + local output_file="$tmp_dir/output" + + : > "$stop_calls_file" + : > "$output_file" + + system_proxy_disable() { return "$proxy_rc"; } + service_stop() { printf 'stop\n' >> "$stop_calls_file"; } + status_is_running() { + [ ! -s "$stop_calls_file" ] + } + + cmd_off > "$output_file" + + if [ "$(wc -l < "$stop_calls_file" | tr -d ' ')" != "1" ]; then + echo "not ok - $name: service_stop was not called exactly once" >&2 + return 1 + fi + + if ! grep -Fq "代理已关闭" "$output_file"; then + echo "not ok - $name: success feedback missing" >&2 + sed 's/^/ /' "$output_file" >&2 + return 1 + fi + + echo "ok - $name" +} + +run_case "stops runtime after disabling system proxy" 0 +run_case "still stops runtime when system proxy cleanup is unsupported" 2 + +run_stop_failure_case() { + local stop_calls_file="$tmp_dir/stop-failure-calls" + local output_file="$tmp_dir/output" + + : > "$stop_calls_file" + : > "$output_file" + + system_proxy_disable() { return 0; } + service_stop() { + printf 'stop\n' >> "$stop_calls_file" + return 7 + } + status_is_running() { return 0; } + + if ( cmd_off ) > "$output_file"; then + echo "not ok - stop failure: cmd_off unexpectedly succeeded" >&2 + return 1 + fi + + if ! grep -Fq "运行后端 stop 返回 7" "$output_file"; then + echo "not ok - stop failure: missing stop failure detail" >&2 + sed 's/^/ /' "$output_file" >&2 + return 1 + fi + + if grep -Fq "代理已关闭" "$output_file"; then + echo "not ok - stop failure: printed success after stop failure" >&2 + sed 's/^/ /' "$output_file" >&2 + return 1 + fi + + echo "ok - stop failure returns non-zero" +} + +run_persistent_cleanup_blocked_case() { + local stop_calls_file="$tmp_dir/persistent-cleanup-blocked-calls" + local output_file="$tmp_dir/output" + + : > "$stop_calls_file" + : > "$output_file" + + system_proxy_status() { printf 'on\n'; } + system_proxy_disable() { return 2; } + service_stop() { printf 'stop\n' >> "$stop_calls_file"; } + status_is_running() { + [ ! -s "$stop_calls_file" ] + } + + if ( cmd_off ) > "$output_file"; then + echo "not ok - persistent cleanup blocked: cmd_off unexpectedly succeeded" >&2 + return 1 + fi + + if [ "$(wc -l < "$stop_calls_file" | tr -d ' ')" != "1" ]; then + echo "not ok - persistent cleanup blocked: service_stop was not called exactly once" >&2 + return 1 + fi + + if ! grep -Fq "系统代理持久块未清理" "$output_file"; then + echo "not ok - persistent cleanup blocked: missing cleanup failure detail" >&2 + sed 's/^/ /' "$output_file" >&2 + return 1 + fi + + if grep -Fq "代理已关闭" "$output_file"; then + echo "not ok - persistent cleanup blocked: printed success while persistent proxy remained" >&2 + sed 's/^/ /' "$output_file" >&2 + return 1 + fi + + echo "ok - persistent cleanup failure returns non-zero after stopping runtime" +} + +run_still_running_case() { + local stop_calls_file="$tmp_dir/still-running-calls" + local output_file="$tmp_dir/output" + + : > "$stop_calls_file" + : > "$output_file" + + system_proxy_disable() { return 0; } + service_stop() { printf 'stop\n' >> "$stop_calls_file"; } + status_is_running() { return 0; } + wait_runtime_stopped() { return 1; } + + if ( cmd_off ) > "$output_file"; then + echo "not ok - still running: cmd_off unexpectedly succeeded" >&2 + return 1 + fi + + if ! grep -Fq "代理内核仍在运行" "$output_file"; then + echo "not ok - still running: missing still-running detail" >&2 + sed 's/^/ /' "$output_file" >&2 + return 1 + fi + + if grep -Fq "代理已关闭" "$output_file"; then + echo "not ok - still running: printed success while runtime stayed up" >&2 + sed 's/^/ /' "$output_file" >&2 + return 1 + fi + + echo "ok - still running returns non-zero" +} + +run_stop_failure_case +run_persistent_cleanup_blocked_case +run_still_running_case