From d1cc7d9b3b41fe0310c2ff2710b490d28ddaaa71 Mon Sep 17 00:00:00 2001 From: Arvin <62139570+wnlen@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:22:59 +0800 Subject: [PATCH] script --- scripts/doctor.sh | 21 +++++ scripts/generate_config.sh | 157 ++++++++++++++++++------------------- scripts/logs.sh | 20 +++++ scripts/run_clash.sh | 19 +++++ scripts/service_lib.sh | 66 ++++++++++++---- scripts/status.sh | 28 +++++++ scripts/stop_clash.sh | 23 ++++++ 7 files changed, 238 insertions(+), 96 deletions(-) diff --git a/scripts/doctor.sh b/scripts/doctor.sh index e69de29..f2a3d43 100644 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -e + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +RUNTIME_DIR="$PROJECT_DIR/runtime" + +echo "=== Clash Doctor ===" + +if [ -f "$RUNTIME_DIR/config.yaml" ]; then + echo "[OK] config exists" +else + echo "[ERROR] config missing" +fi + +if command -v systemctl >/dev/null 2>&1; then + if systemctl is-active --quiet clash-for-linux.service; then + echo "[OK] service running" + else + echo "[WARN] service not running" + fi +fi \ No newline at end of file diff --git a/scripts/generate_config.sh b/scripts/generate_config.sh index a33d64f..d0a7549 100644 --- a/scripts/generate_config.sh +++ b/scripts/generate_config.sh @@ -3,19 +3,22 @@ set -euo pipefail PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RUNTIME_DIR="$PROJECT_DIR/runtime" -CONF_DIR="$PROJECT_DIR/conf" -TEMP_DIR="$PROJECT_DIR/temp" +CONFIG_DIR="$PROJECT_DIR/config" LOG_DIR="$PROJECT_DIR/logs" RUNTIME_CONFIG="$RUNTIME_DIR/config.yaml" STATE_FILE="$RUNTIME_DIR/state.env" -TEMP_DOWNLOAD="$TEMP_DIR/clash.yaml" -TEMP_CONVERTED="$TEMP_DIR/clash_config.yaml" -mkdir -p "$RUNTIME_DIR" "$CONF_DIR" "$TEMP_DIR" "$LOG_DIR" +TMP_DOWNLOAD="$RUNTIME_DIR/subscription.raw.yaml" +TMP_NORMALIZED="$RUNTIME_DIR/subscription.normalized.yaml" +TMP_PROXY_FRAGMENT="$RUNTIME_DIR/proxy.fragment.yaml" -# shellcheck disable=SC1091 -source "$PROJECT_DIR/.env" +mkdir -p "$RUNTIME_DIR" "$CONFIG_DIR" "$LOG_DIR" + +if [ -f "$PROJECT_DIR/.env" ]; then + # shellcheck disable=SC1091 + source "$PROJECT_DIR/.env" +fi # shellcheck disable=SC1091 source "$PROJECT_DIR/scripts/get_cpu_arch.sh" @@ -58,8 +61,9 @@ EOF generate_secret() { if [ -n "${CLASH_SECRET:-}" ]; then echo "$CLASH_SECRET" - return + return 0 fi + if command -v openssl >/dev/null 2>&1; then openssl rand -hex 16 else @@ -69,8 +73,11 @@ generate_secret() { SECRET="$(generate_secret)" -upsert_yaml_kv() { - local file="$1" key="$2" value="$3" +upsert_yaml_kv_local() { + local file="$1" + local key="$2" + local value="$3" + [ -f "$file" ] || touch "$file" if grep -qE "^[[:space:]]*${key}:" "$file"; then @@ -80,110 +87,102 @@ upsert_yaml_kv() { fi } -force_write_secret() { +apply_secret_to_config() { local file="$1" - upsert_yaml_kv "$file" "secret" "$SECRET" + upsert_yaml_kv_local "$file" "secret" "$SECRET" } -force_write_controller_and_ui() { +apply_controller_to_config() { local file="$1" if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then - upsert_yaml_kv "$file" "external-controller" "$EXTERNAL_CONTROLLER" + upsert_yaml_kv_local "$file" "external-controller" "$EXTERNAL_CONTROLLER" mkdir -p "$RUNTIME_DIR" ln -sfn "$PROJECT_DIR/dashboard/public" "$RUNTIME_DIR/ui" - upsert_yaml_kv "$file" "external-ui" "$RUNTIME_DIR/ui" + upsert_yaml_kv_local "$file" "external-ui" "$RUNTIME_DIR/ui" fi } download_subscription() { [ -n "$CLASH_URL" ] || return 1 - local curl_cmd=(curl -fL -S --retry 2 --connect-timeout 10 -m 30 -o "$TEMP_DOWNLOAD") + local curl_cmd=(curl -fL -S --retry 2 --connect-timeout 10 -m 30 -o "$TMP_DOWNLOAD") [ "$ALLOW_INSECURE_TLS" = "true" ] && curl_cmd+=(-k) curl_cmd+=("$CLASH_URL") "${curl_cmd[@]}" } -use_fallback() { - [ -s "$CONF_DIR/fallback_config.yaml" ] || return 1 - cp -f "$CONF_DIR/fallback_config.yaml" "$RUNTIME_CONFIG" - force_write_controller_and_ui "$RUNTIME_CONFIG" - force_write_secret "$RUNTIME_CONFIG" -} - -is_full_config() { +is_complete_clash_config() { local file="$1" grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$file" } +cleanup_tmp_files() { + rm -f "$TMP_NORMALIZED" "$TMP_PROXY_FRAGMENT" +} + main() { - if [ "$CLASH_AUTO_UPDATE" != "true" ]; then - if [ -s "$RUNTIME_CONFIG" ]; then - write_state "success" "auto_update_disabled_keep_runtime" "runtime_existing" - exit 0 - fi - use_fallback - write_state "success" "auto_update_disabled_use_fallback" "fallback" - exit 0 + local template_file="$CONFIG_DIR/template.yaml" + + if [ "$CLASH_AUTO_UPDATE" != "true" ]; then + if [ -s "$RUNTIME_CONFIG" ]; then + write_state "success" "auto_update_disabled_keep_runtime" "runtime_existing" + exit 0 fi - if ! download_subscription; then - if [ -s "$RUNTIME_CONFIG" ]; then - write_state "success" "download_failed_keep_last_good" "runtime_existing" - exit 0 - fi - use_fallback - write_state "success" "download_failed_use_fallback" "fallback" - exit 0 - fi - - cp -f "$TEMP_DOWNLOAD" "$TEMP_CONVERTED" - - if is_full_config "$TEMP_CONVERTED"; then - cp -f "$TEMP_CONVERTED" "$RUNTIME_CONFIG" - force_write_controller_and_ui "$RUNTIME_CONFIG" - force_write_secret "$RUNTIME_CONFIG" - write_state "success" "subscription_full" "subscription_full" - exit 0 - fi - - # 片段订阅:这里先保留模板拼接逻辑 - TEMPLATE_FILE="" - - if [ -s "$CONF_DIR/template.yaml" ]; then - TEMPLATE_FILE="$CONF_DIR/template.yaml" - elif [ -s "$TEMP_DIR/template.yaml" ]; then - TEMPLATE_FILE="$TEMP_DIR/template.yaml" - elif [ -s "$CONF_DIR/template.yaml" ]; then - TEMPLATE_FILE="$CONF_DIR/template.yaml" - elif [ -s "$PROJECT_DIR/temp/template.yaml" ]; then - TEMPLATE_FILE="$PROJECT_DIR/temp/template.yaml" - fi - - if [ -z "$TEMPLATE_FILE" ]; then - echo "[ERROR] missing template config file (template.yaml / template.yaml)" >&2 - write_state "failed" "missing_template" "none" + echo "[ERROR] auto update disabled and runtime config missing: $RUNTIME_CONFIG" >&2 + write_state "failed" "runtime_missing" "none" exit 1 + fi + + if ! download_subscription; then + if [ -s "$RUNTIME_CONFIG" ]; then + write_state "success" "download_failed_keep_runtime" "runtime_existing" + exit 0 fi - sed -n '/^proxies:/,$p' "$TEMP_CONVERTED" > "$TEMP_DIR/proxy.txt" - cat "$TEMPLATE_FILE" > "$RUNTIME_CONFIG" - cat "$TEMP_DIR/proxy.txt" >> "$RUNTIME_CONFIG" + echo "[ERROR] failed to download subscription and runtime config missing" >&2 + write_state "failed" "download_failed" "none" + exit 1 + fi - sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$RUNTIME_CONFIG" - sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$RUNTIME_CONFIG" - sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$RUNTIME_CONFIG" - sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$RUNTIME_CONFIG" - sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$RUNTIME_CONFIG" + cp -f "$TMP_DOWNLOAD" "$TMP_NORMALIZED" - force_write_controller_and_ui "$RUNTIME_CONFIG" - force_write_secret "$RUNTIME_CONFIG" + if is_complete_clash_config "$TMP_NORMALIZED"; then + cp -f "$TMP_NORMALIZED" "$RUNTIME_CONFIG" + apply_controller_to_config "$RUNTIME_CONFIG" + apply_secret_to_config "$RUNTIME_CONFIG" + write_state "success" "subscription_full" "subscription_full" + cleanup_tmp_files + exit 0 + fi - write_state "success" "subscription_fragment_merged" "subscription_fragment" + if [ ! -s "$template_file" ]; then + echo "[ERROR] missing template config file: $template_file" >&2 + write_state "failed" "missing_template" "none" + cleanup_tmp_files + exit 1 + fi + + sed -n '/^proxies:/,$p' "$TMP_NORMALIZED" > "$TMP_PROXY_FRAGMENT" + + cat "$template_file" > "$RUNTIME_CONFIG" + cat "$TMP_PROXY_FRAGMENT" >> "$RUNTIME_CONFIG" + + sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$RUNTIME_CONFIG" + sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$RUNTIME_CONFIG" + sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$RUNTIME_CONFIG" + sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$RUNTIME_CONFIG" + sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$RUNTIME_CONFIG" + + apply_controller_to_config "$RUNTIME_CONFIG" + apply_secret_to_config "$RUNTIME_CONFIG" + + write_state "success" "subscription_fragment_merged" "subscription_fragment" + cleanup_tmp_files } main "$@" \ No newline at end of file diff --git a/scripts/logs.sh b/scripts/logs.sh index e69de29..8de5000 100644 --- a/scripts/logs.sh +++ b/scripts/logs.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -e + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +LOG_FILE="$PROJECT_DIR/logs/clash.log" +SERVICE_NAME="clash-for-linux.service" + +if [ "${1:-}" = "-f" ]; then + if command -v journalctl >/dev/null 2>&1; then + journalctl -u "$SERVICE_NAME" -f + else + tail -f "$LOG_FILE" + fi +else + if command -v journalctl >/dev/null 2>&1; then + journalctl -u "$SERVICE_NAME" -n 50 --no-pager + else + tail -n 50 "$LOG_FILE" + fi +fi \ No newline at end of file diff --git a/scripts/run_clash.sh b/scripts/run_clash.sh index 749ae7e..f671bec 100644 --- a/scripts/run_clash.sh +++ b/scripts/run_clash.sh @@ -41,10 +41,29 @@ fi # shellcheck disable=SC1091 source "$PROJECT_DIR/scripts/get_cpu_arch.sh" +# shellcheck disable=SC1091 source "$PROJECT_DIR/scripts/resolve_clash.sh" +# shellcheck disable=SC1091 +source "$PROJECT_DIR/scripts/service_lib.sh" CLASH_BIN="$(resolve_clash_bin "$PROJECT_DIR" "${CpuArch:-}")" +if [ ! -x "$CLASH_BIN" ]; then + echo "[ERROR] clash binary not found or not executable: $CLASH_BIN" >&2 + exit 2 +fi + +test_config() { + local bin="$1" + local config="$2" + "$bin" -t -f "$config" >/dev/null 2>&1 +} + +if ! test_config "$CLASH_BIN" "$CONFIG_FILE"; then + echo "[ERROR] config test failed: $CONFIG_FILE" >&2 + exit 2 +fi + # systemd 模式 if [ "$FOREGROUND" = true ]; then write_run_state "running" "systemd" diff --git a/scripts/service_lib.sh b/scripts/service_lib.sh index 616389c..2dda03e 100644 --- a/scripts/service_lib.sh +++ b/scripts/service_lib.sh @@ -4,6 +4,7 @@ set -euo pipefail PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RUNTIME_DIR="$PROJECT_DIR/runtime" PID_FILE="$RUNTIME_DIR/clash.pid" +STATE_FILE="$RUNTIME_DIR/state.env" SERVICE_NAME="clash-for-linux.service" mkdir -p "$RUNTIME_DIR" @@ -17,6 +18,17 @@ service_unit_exists() { systemctl show "$SERVICE_NAME" -p LoadState --value 2>/dev/null | grep -q '^loaded$' } +read_pid() { + [ -f "$PID_FILE" ] || return 1 + cat "$PID_FILE" +} + +is_script_running() { + local pid + pid="$(read_pid 2>/dev/null || true)" + [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null +} + detect_mode() { if service_unit_exists && systemctl is-active --quiet "$SERVICE_NAME"; then echo "systemd" @@ -29,15 +41,38 @@ detect_mode() { fi } -read_pid() { - [ -f "$PID_FILE" ] || return 1 - cat "$PID_FILE" -} +write_run_state() { + local status="$1" + local mode="${2:-unknown}" + local pid="${3:-}" -is_script_running() { - local pid - pid="$(read_pid 2>/dev/null || true)" - [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null + touch "$STATE_FILE" + + if grep -q '^LAST_RUN_STATUS=' "$STATE_FILE" 2>/dev/null; then + sed -i -E "s/^LAST_RUN_STATUS=.*/LAST_RUN_STATUS=${status}/" "$STATE_FILE" + else + echo "LAST_RUN_STATUS=${status}" >> "$STATE_FILE" + fi + + if grep -q '^LAST_RUN_MODE=' "$STATE_FILE" 2>/dev/null; then + sed -i -E "s/^LAST_RUN_MODE=.*/LAST_RUN_MODE=${mode}/" "$STATE_FILE" + else + echo "LAST_RUN_MODE=${mode}" >> "$STATE_FILE" + fi + + if grep -q '^LAST_RUN_AT=' "$STATE_FILE" 2>/dev/null; then + sed -i -E "s/^LAST_RUN_AT=.*/LAST_RUN_AT=$(date -Iseconds)/" "$STATE_FILE" + else + echo "LAST_RUN_AT=$(date -Iseconds)" >> "$STATE_FILE" + fi + + if [ -n "$pid" ]; then + if grep -q '^LAST_RUN_PID=' "$STATE_FILE" 2>/dev/null; then + sed -i -E "s/^LAST_RUN_PID=.*/LAST_RUN_PID=${pid}/" "$STATE_FILE" + else + echo "LAST_RUN_PID=${pid}" >> "$STATE_FILE" + fi + fi } start_via_systemd() { @@ -46,6 +81,8 @@ start_via_systemd() { stop_via_systemd() { systemctl stop "$SERVICE_NAME" + write_run_state "stopped" "systemd" + rm -f "$PID_FILE" } restart_via_systemd() { @@ -63,6 +100,7 @@ start_via_script() { stop_via_script() { local pid pid="$(read_pid 2>/dev/null || true)" + if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then echo "[INFO] stopping clash pid=$pid" kill "$pid" @@ -71,16 +109,10 @@ stop_via_script() { kill -9 "$pid" 2>/dev/null || true fi fi - rm -f "$PID_FILE" - if [ -f "$PROJECT_DIR/runtime/state.env" ]; then - if grep -q '^LAST_RUN_STATUS=' "$PROJECT_DIR/runtime/state.env" 2>/dev/null; then - sed -i -E "s/^LAST_RUN_STATUS=.*/LAST_RUN_STATUS=stopped/" "$PROJECT_DIR/runtime/state.env" - else - echo "LAST_RUN_STATUS=stopped" >> "$PROJECT_DIR/runtime/state.env" - fi - fi -} + rm -f "$PID_FILE" + write_run_state "stopped" "script" +} restart_via_script() { stop_via_script || true diff --git a/scripts/status.sh b/scripts/status.sh index e69de29..c31403d 100644 --- a/scripts/status.sh +++ b/scripts/status.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -e + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +# shellcheck disable=SC1091 +source "$PROJECT_DIR/scripts/service_lib.sh" + +mode="$(detect_mode)" + +echo "=== Clash Status ===" +echo "Project : $PROJECT_DIR" +echo "Mode : $mode" + +case "$mode" in + systemd) + echo "Running : yes (systemd)" + ;; + script) + echo "Running : yes (script)" + ;; + systemd-installed) + echo "Running : no (installed but not started)" + ;; + *) + echo "Running : no" + ;; +esac \ No newline at end of file diff --git a/scripts/stop_clash.sh b/scripts/stop_clash.sh index e69de29..d045ffe 100644 --- a/scripts/stop_clash.sh +++ b/scripts/stop_clash.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -e + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +# shellcheck disable=SC1091 +source "$PROJECT_DIR/scripts/service_lib.sh" + +mode="$(detect_mode)" + +case "$mode" in + systemd) + stop_via_systemd + echo "[OK] stopped via systemd" + ;; + script) + stop_via_script + echo "[OK] stopped via script" + ;; + *) + echo "[WARN] nothing is running" + ;; +esac \ No newline at end of file