From 7d950698f153406f6b993d467216c6dae93f9080 Mon Sep 17 00:00:00 2001 From: wnlen <544241974@qq.com> Date: Mon, 16 Mar 2026 23:58:51 +0800 Subject: [PATCH] Update install.sh --- install.sh | 1328 +++++++++++++++++++--------------------------------- 1 file changed, 483 insertions(+), 845 deletions(-) diff --git a/install.sh b/install.sh index 4dad35e..cec8d78 100755 --- a/install.sh +++ b/install.sh @@ -1,904 +1,542 @@ -#!/usr/bin/env bash -# 严格模式 +#!/bin/bash set -euo pipefail -# --- DEBUG: 打印具体失败的行号和命令(systemd 下非常关键) --- -trap 'rc=$?; echo "[ERR] rc=$rc line=$LINENO cmd=$BASH_COMMAND" >&2' ERR -# 如需更详细:取消下一行注释 -# set -x -# --- DEBUG end --- +# ========================= +# 基础参数 +# ========================= +Server_Dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +Install_Dir="${CLASH_INSTALL_DIR:-$Server_Dir}" +Service_Name="clash-for-linux" +Service_User="root" +Service_Group="root" -############################################ -# Clash for Linux - start.sh (Full Version) -# - systemd 模式下订阅失败/下载失败:不退出,使用 conf/config.yaml(必要时从 conf/fallback_config.yaml 拷贝)兜底启动 -# - 非 systemd 模式:订阅失败/下载失败直接退出(保持手动执行的强约束) -############################################ +# ========================= +# 彩色输出(统一 printf + 自动降级 + 手动关色) +# ========================= -# 加载系统函数库(Only for RHEL Linux) -[ -f /etc/init.d/functions ] && source /etc/init.d/functions +# ---- 关色开关(优先级最高)---- +NO_COLOR_FLAG=0 +for arg in "$@"; do + case "$arg" in + --no-color|--nocolor) + NO_COLOR_FLAG=1 + ;; + esac +done -#################### 脚本初始化任务 #################### - -# 获取脚本工作目录绝对路径 -export Server_Dir -Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# 加载.env变量文件 -# shellcheck disable=SC1090 -# --- source .env(不可信输入,必须放宽) --- -if [ -f "$Server_Dir/.env" ]; then - set +u - source "$Server_Dir/.env" || echo "[WARN] failed to source .env" >&2 - set -u +if [[ -n "${NO_COLOR:-}" ]] || [[ -n "${CLASH_NO_COLOR:-}" ]]; then + NO_COLOR_FLAG=1 fi -# systemd 模式开关(必须在 set -u 下安全) -SYSTEMD_MODE="${SYSTEMD_MODE:-false}" - -# root-only 强约束:不是 root 直接退出 -if [ "$(id -u)" -ne 0 ]; then - echo "[ERR] root-only mode: please run as root" >&2 - exit 2 +# ---- 初始化颜色 ---- +if [[ "$NO_COLOR_FLAG" -eq 0 ]] && [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then + if tput setaf 1 >/dev/null 2>&1; then + C_RED="$(tput setaf 1)" + C_GREEN="$(tput setaf 2)" + C_YELLOW="$(tput setaf 3)" + C_BLUE="$(tput setaf 4)" + C_CYAN="$(tput setaf 6)" + C_GRAY="$(tput setaf 8 2>/dev/null || true)" + C_BOLD="$(tput bold)" + C_UL="$(tput smul)" + C_NC="$(tput sgr0)" + fi fi -# 给二进制启动程序、脚本等添加可执行权限 -chmod +x "$Server_Dir/bin/"* 2>/dev/null || true -chmod +x "$Server_Dir/scripts/"* 2>/dev/null || true -if [ -f "$Server_Dir/tools/subconverter/subconverter" ]; then - chmod +x "$Server_Dir/tools/subconverter/subconverter" 2>/dev/null || true +# ---- ANSI fallback ---- +if [[ "$NO_COLOR_FLAG" -eq 0 ]] && [[ -t 1 ]] && [[ -z "${C_NC:-}" ]]; then + C_RED=$'\033[31m' + C_GREEN=$'\033[32m' + C_YELLOW=$'\033[33m' + C_BLUE=$'\033[34m' + C_CYAN=$'\033[36m' + C_GRAY=$'\033[90m' + C_BOLD=$'\033[1m' + C_UL=$'\033[4m' + C_NC=$'\033[0m' fi -#################### 变量设置 #################### +# ---- 强制无色 ---- +if [[ "$NO_COLOR_FLAG" -eq 1 ]] || [[ ! -t 1 ]]; then + C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' C_CYAN='' C_GRAY='' C_BOLD='' C_UL='' C_NC='' +fi -Conf_Dir="$Server_Dir/conf" +# ========================= +# 基础输出函数 +# ========================= +log() { printf "%b\n" "$*"; } +info() { log "${C_CYAN}[INFO]${C_NC} $*"; } +ok() { log "${C_GREEN}[OK]${C_NC} $*"; } +warn() { log "${C_YELLOW}[WARN]${C_NC} $*"; } +err() { log "${C_RED}[ERROR]${C_NC} $*"; } -# root-only:统一使用安装目录下的 temp/logs -Temp_Dir="$Server_Dir/temp" -Log_Dir="$Server_Dir/logs" +# ========================= +# 样式助手 +# ========================= +path() { printf "%b" "${C_BOLD}$*${C_NC}"; } +cmd() { printf "%b" "${C_GRAY}$*${C_NC}"; } +url() { printf "%b" "${C_UL}$*${C_NC}"; } +good() { printf "%b" "${C_GREEN}$*${C_NC}"; } +bad() { printf "%b" "${C_RED}$*${C_NC}"; } -mkdir -p "$Conf_Dir" "$Temp_Dir" "$Log_Dir" || { - echo "[ERR] cannot create dirs: Conf_Dir=$Conf_Dir Temp_Dir=$Temp_Dir Log_Dir=$Log_Dir" - exit 2 +# ========================= +# 分段标题(CLI 风格 section) +# ========================= +section() { + local title="$*" + log "" + log "${C_BOLD}▶ ${title}${C_NC}" + log "${C_GRAY}────────────────────────────────────────${C_NC}" } -# 再做一次可写性检查,避免后面玄学 exit -touch "$Temp_Dir/.write_test" 2>/dev/null || { echo "[ERR] Temp_Dir not writable: $Temp_Dir"; exit 2; } -rm -f "$Temp_Dir/.write_test" 2>/dev/null || true +# ========================= +# 前置校验 +# ========================= +if [ "$(id -u)" -ne 0 ]; then + err "需要 root 权限执行安装脚本(请使用 sudo bash install.sh)" + exit 1 +fi -PID_FILE="${CLASH_PID_FILE:-$Temp_Dir/clash.pid}" +if [ ! -f "${Server_Dir}/.env" ]; then + err "未找到 .env 文件,请确认脚本所在目录:${Server_Dir}" + exit 1 +fi -is_running() { - if [ -f "$PID_FILE" ]; then - local pid - pid="$(cat "$PID_FILE" 2>/dev/null || true)" - if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then - return 0 +# ========================= +# 同步到安装目录(保持你原逻辑) +# ========================= +mkdir -p "$Install_Dir" +if [ "$Server_Dir" != "$Install_Dir" ]; then + info "同步项目文件到安装目录:${Install_Dir}" + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete --exclude '.git' "$Server_Dir/" "$Install_Dir/" + else + cp -a "$Server_Dir/." "$Install_Dir/" + fi +fi + +chmod +x "$Install_Dir"/*.sh 2>/dev/null || true +chmod +x "$Install_Dir"/scripts/* 2>/dev/null || true +chmod +x "$Install_Dir"/bin/* 2>/dev/null || true +chmod +x "$Install_Dir"/clashctl 2>/dev/null || true + +# ========================= +# 加载环境与依赖脚本 +# ========================= +# shellcheck disable=SC1090 +source "$Install_Dir/.env" +# shellcheck disable=SC1090 +source "$Install_Dir/scripts/get_cpu_arch.sh" +# shellcheck disable=SC1090 +source "$Install_Dir/scripts/resolve_clash.sh" +# shellcheck disable=SC1090 +source "$Install_Dir/scripts/port_utils.sh" + +if [[ -z "${CpuArch:-}" ]]; then + err "无法识别 CPU 架构" + exit 1 +fi + +# ========================= +# .env 写入工具:write_env_kv(必须在 prompt 之前定义) +# - 自动创建文件 +# - 存在则替换,不存在则追加 +# - 统一写成:export KEY="VALUE" +# - 自动转义双引号/反斜杠 +# ========================= +escape_env_value() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +write_env_kv() { + local file="$1" + local key="$2" + local val="$3" + + mkdir -p "$(dirname "$file")" 2>/dev/null || true + [ -f "$file" ] || touch "$file" + + val="$(printf '%s' "$val" | tr -d '\r')" + local esc + esc="$(escape_env_value "$val")" + + if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$file"; then + sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*|export ${key}=\"${esc}\"|g" "$file" + else + printf 'export %s="%s"\n' "$key" "$esc" >> "$file" + fi +} + +# ========================= +# 交互式填写订阅地址(仅在 CLASH_URL 为空时触发) +# - 若非 TTY(CI/管道)则跳过交互 +# - 若用户回车跳过,则保持原行为:装完提示手动配置 +# ========================= +prompt_clash_url_if_empty() { + # 兼容 .env 里可能是 CLASH_URL= / export CLASH_URL= / 带引号 + local cur="${CLASH_URL:-}" + cur="${cur%\"}"; cur="${cur#\"}" + + if [ -n "$cur" ]; then + return 0 + fi + + # 非交互环境:不阻塞 + if [ ! -t 0 ]; then + warn "CLASH_URL 为空且当前为非交互环境(stdin 非 TTY),将跳过输入引导。" + return 0 + fi + + echo + warn "未检测到订阅地址(CLASH_URL 为空)" + echo "请粘贴你的 Clash 订阅地址(直接回车跳过,稍后手动编辑 .env):" + read -r -p "Clash URL: " input_url + + input_url="$(printf '%s' "$input_url" | tr -d '\r')" + + # 回车跳过:保持原行为(不写入) + if [ -z "$input_url" ]; then + warn "已跳过填写订阅地址,安装完成后请手动编辑:${Install_Dir}/.env" + return 0 + fi + + # 先校验再写入,避免污染 .env + if ! echo "$input_url" | grep -Eq '^https?://'; then + err "订阅地址格式不正确(必须以 http:// 或 https:// 开头)" + exit 1 + fi + + ENV_FILE="${Install_Dir}/.env" + mkdir -p "$Install_Dir" + [ -f "$ENV_FILE" ] || touch "$ENV_FILE" + + # ✅ 只用这一套写入逻辑(统一 export KEY="...",兼容旧格式) + write_env_kv "$ENV_FILE" "CLASH_URL" "$input_url" + + export CLASH_URL="$input_url" + ok "已写入订阅地址到:${ENV_FILE}" +} + +prompt_clash_url_if_empty + +# ========================= +# 端口冲突检测(保持你原逻辑) +# ========================= +CLASH_HTTP_PORT=${CLASH_HTTP_PORT:-7890} +CLASH_SOCKS_PORT=${CLASH_SOCKS_PORT:-7891} +CLASH_REDIR_PORT=${CLASH_REDIR_PORT:-7892} +EXTERNAL_CONTROLLER=${EXTERNAL_CONTROLLER:-127.0.0.1:9090} + +parse_port() { + local raw="$1" + raw="${raw##*:}" + echo "$raw" +} + +Port_Conflicts=() +for port in "$CLASH_HTTP_PORT" "$CLASH_SOCKS_PORT" "$CLASH_REDIR_PORT" "$(parse_port "$EXTERNAL_CONTROLLER")"; do + if [ "$port" = "auto" ] || [ -z "$port" ]; then + continue + fi + if [[ "$port" =~ ^[0-9]+$ ]]; then + if is_port_in_use "$port"; then + Port_Conflicts+=("$port") fi fi +done + +if [ "${#Port_Conflicts[@]}" -ne 0 ]; then + warn "检测到端口冲突: ${Port_Conflicts[*]},运行时将自动分配可用端口" +fi + +install -d -m 0755 "$Install_Dir/conf" "$Install_Dir/logs" "$Install_Dir/temp" + +# ========================= +# Clash 内核就绪检查/下载 +# ========================= +if ! resolve_clash_bin "$Install_Dir" "$CpuArch" >/dev/null 2>&1; then + err "Clash 内核未就绪,请检查下载配置或手动放置二进制" + exit 1 +fi + +# ========================= +# fonction 工具函数区 +# ========================= +# 等待 config.yaml 出现并写入 secret(默认最多等 6 秒) +wait_secret_ready() { + local conf_file="$1" + local timeout_sec="${2:-6}" + + local end=$((SECONDS + timeout_sec)) + while [ "$SECONDS" -lt "$end" ]; do + if [ -s "$conf_file" ] && grep -qE '^[[:space:]]*secret:' "$conf_file"; then + return 0 + fi + sleep 0.2 + done return 1 } -if [ "${SYSTEMD_MODE:-false}" != "true" ] && is_running; then - echo -e "\n[OK] Clash 已在运行 (pid=$(cat "$PID_FILE")),跳过重复启动\n" - exit 0 -fi - -# systemd 模式下避免读取遗留 pid 干扰判断 -if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - rm -f "$PID_FILE" 2>/dev/null || true -fi - -# 统一订阅变量 -URL="${CLASH_URL:-}" - -# 清理可能的 CRLF(Windows 写 .env 很常见) -URL="$(printf '%s' "$URL" | tr -d '\r')" -URL="$(printf '%s' "$URL" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" - -#让 bash 子进程能拿到 -export CLASH_URL="$URL" - -# 只有在“需要在线更新订阅”的模式下才强制要求 URL -if [ -z "$URL" ] && [ "${SYSTEMD_MODE:-false}" != "true" ]; then - echo "[ERR] CLASH_URL 为空(未配置订阅地址)" - exit 2 -fi -if [ -n "$URL" ] && ! printf '%s' "$URL" | grep -Eq '^https?://'; then - echo "[ERR] CLASH_URL 格式无效:必须以 http:// 或 https:// 开头" >&2 - exit 2 -fi - -# 获取 CLASH_SECRET 值:优先 .env;其次读取旧 config;占位符视为无效;最后生成随机值 -Secret="${CLASH_SECRET:-}" - -# 尝试从旧 config.yaml 读取(仅当 .env 未提供) -if [ -z "$Secret" ] && [ -f "$Conf_Dir/config.yaml" ]; then - Secret="$(awk -F': *' '/^[[:space:]]*secret[[:space:]]*:/{print $2; exit}' "$Conf_Dir/config.yaml" 2>/dev/null | tr -d '"' || true)" -fi - -# 若读取到的是占位符(如 ${CLASH_SECRET}),视为无效 -if [[ "$Secret" =~ ^\$\{.*\}$ ]]; then - Secret="" -fi - -# 兜底生成随机 secret -if [ -z "$Secret" ]; then - if command -v openssl >/dev/null 2>&1; then - Secret="$(openssl rand -hex 32)" - else - # 32 bytes -> 64 hex chars - Secret="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')" - fi -fi - -# 强制写入 secret 到指定配置文件(存在则替换,不存在则追加) -force_write_secret() { - local file="$1" - [ -f "$file" ] || return 0 - - if grep -qE '^[[:space:]]*secret:' "$file"; then - # 替换整行 secret(无论原来是啥,包括 SECRET_PLACEHOLDER / "${CLASH_SECRET}") - sed -i -E "s|^[[:space:]]*secret:.*$|secret: ${Secret}|g" "$file" - else - # 没有 secret 行就追加到文件末尾 - printf "\nsecret: %s\n" "$Secret" >> "$file" - fi +# 计算字符串可视宽度:中文大概率按 2 宽处理(简单够用版) +# 注:终端宽度/字体不统一时,中文宽度估算永远只能“近似” +vis_width() { + python3 - <<'PY' "$1" +import sys +s=sys.argv[1] +w=0 +for ch in s: + # East Asian Wide/FullWidth 近似当 2 + w += 2 if ord(ch) >= 0x2E80 else 1 +print(w) +PY } -ensure_ui_link() { - mkdir -p "$Conf_Dir" - ln -sfn "$Server_Dir/dashboard/public" "$Conf_Dir/ui" +pad_right() { # pad_right "text" width + local s="$1" w="$2" + local cur + cur="$(vis_width "$s")" + local pad=$(( w - cur )) + (( pad < 0 )) && pad=0 + printf "%s%*s" "$s" "$pad" "" } -# --- helpers: upsert yaml key (top-level), ensure UI links --- -upsert_yaml_kv() { - # Usage: upsert_yaml_kv - # Writes: key: value (top-level) - local file="$1" key="$2" value="$3" - [ -n "$file" ] && [ -n "$key" ] || return 1 - - # 如果文件不存在,先创建 - [ -f "$file" ] || : >"$file" || return 1 - - if grep -qE "^[[:space:]]*${key}:[[:space:]]*" "$file" 2>/dev/null; then - # 替换整行(避免残留引号) - sed -i -E "s|^[[:space:]]*${key}:[[:space:]]*.*$|${key}: ${value}|g" "$file" - else - # 追加前保证有换行 - tail -c 1 "$file" 2>/dev/null | read -r _last || true - # shellcheck disable=SC2034 - if [ "$(tail -c 1 "$file" 2>/dev/null || true)" != "" ]; then - printf "\n" >>"$file" - fi - printf "%s: %s\n" "$key" "$value" >>"$file" - fi +box_title() { # box_title "标题" width + local title="$1" width="$2" + local inner=$((width-2)) + printf "┌%s┐\n" "$(printf '─%.0s' $(seq 1 $inner))" + # 标题居中(近似) + local t=" $title " + local tw; tw="$(vis_width "$t")" + local left=$(( (inner - tw)/2 )); ((left<0)) && left=0 + local right=$(( inner - tw - left )); ((right<0)) && right=0 + printf "│%*s%s%*s│\n" "$left" "" "$t" "$right" "" + printf "├%s┤\n" "$(printf '─%.0s' $(seq 1 $inner))" } -ensure_ui_links() { - local ui_src="${UI_SRC_DIR:-$Server_Dir/dashboard/public}" - mkdir -p "$Conf_Dir" 2>/dev/null || true - if [ -d "$ui_src" ]; then - ln -sfn "$ui_src" "$Conf_Dir/ui" 2>/dev/null || true - fi +box_row() { # box_row "key" "value" width keyw + local k="$1" v="$2" width="$3" keyw="$4" + local inner=$((width-2)) + # 形如:│ key: value │ + local left="$(pad_right "$k" "$keyw")" + local line=" ${left} ${v}" + local lw; lw="$(vis_width "$line")" + local pad=$(( inner - lw )); ((pad<0)) && pad=0 + printf "│%s%*s│\n" "$line" "$pad" "" } -force_write_controller_and_ui() { - local file="$1" - local controller="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" - - [ -n "$file" ] || return 1 - - # external-controller - upsert_yaml_kv "$file" "external-controller" "$controller" || true - - # external-ui: fixed to Conf_Dir/ui - ensure_ui_links - if [ -e "$Conf_Dir/ui" ]; then - upsert_yaml_kv "$file" "external-ui" "$Conf_Dir/ui" || true - fi +box_end() { # box_end width + local width="$1" inner=$((width-2)) + printf "└%s┘\n" "$(printf '─%.0s' $(seq 1 $inner))" } +# 从 config.yaml 提取 secret(强韧:支持缩进/引号/CRLF/尾空格) +read_secret_from_config() { + local conf_file="$1" + [ -f "$conf_file" ] || return 1 -fix_external_ui_by_safe_paths() { - local bin="$1" - local cfg="$2" - local test_out="$3" - local ui_src="${UI_SRC_DIR:-$Server_Dir/dashboard/public}" + # 1) 找到 secret 行 -> 2) 去掉 key 和空格 -> 3) 去掉首尾引号 -> 4) 去掉 CR + local s + s="$( + sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' "$conf_file" \ + | head -n 1 \ + | sed -E 's/^[[:space:]]*"(.*)"[[:space:]]*$/\1/; s/^[[:space:]]*'\''(.*)'\''[[:space:]]*$/\1/' \ + | tr -d '\r' + )" - [ -x "$bin" ] || return 0 - [ -s "$cfg" ] || return 0 + # 去掉纯空格 + s="$(printf '%s' "$s" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" - # 先跑一次 test,把原因写入 test_out - "$bin" -t -f "$cfg" >"$test_out" 2>&1 - local rc=$? - [ $rc -eq 0 ] && return 0 - - # 只处理 external-ui 的 SAFE_PATH 报错 - if ! grep -q "SAFE_PATHS" "$test_out"; then - return $rc - fi - if ! grep -q "external-ui" "$cfg" && ! grep -q "external-ui" "$test_out"; then - return $rc - fi - - # 从 test_out 抽取 allowed paths 的第一个 base - # 例:allowed paths: [/opt/clash-for-linux/.config/mihomo] - local base - base="$(sed -n 's/.*allowed paths: \[\([^]]*\)\].*/\1/p' "$test_out" | head -n 1)" - - [ -n "$base" ] || return $rc - - # external-ui 必须在 allowed base 的子目录里 - local ui_dst="$base/ui" - mkdir -p "$ui_dst" 2>/dev/null || true - - # 把 UI 文件同步过去(真实目录,不用软链,避免跳出 base) - if [ -d "$ui_src" ]; then - if command -v rsync >/dev/null 2>&1; then - rsync -a --delete "$ui_src"/ "$ui_dst"/ 2>/dev/null || true - else - rm -rf "$ui_dst"/* 2>/dev/null || true - cp -a "$ui_src"/. "$ui_dst"/ 2>/dev/null || true - fi - fi - - # 重写 external-ui 到新目录 - upsert_yaml_kv "$cfg" "external-ui" "$ui_dst" || true - - # 再 test 一次 - "$bin" -t -f "$cfg" >"$test_out" 2>&1 - return $? + [ -n "$s" ] || return 1 + printf '%s' "$s" } -# 设置默认值 -CLASH_HTTP_PORT="${CLASH_HTTP_PORT:-7890}" -CLASH_SOCKS_PORT="${CLASH_SOCKS_PORT:-7891}" -CLASH_REDIR_PORT="${CLASH_REDIR_PORT:-7892}" -CLASH_LISTEN_IP="${CLASH_LISTEN_IP:-127.0.0.1}" -CLASH_ALLOW_LAN="${CLASH_ALLOW_LAN:-false}" +# ========================= +# systemd 安装与启动 +# ========================= +Service_Enabled="unknown" +Service_Started="unknown" -EXTERNAL_CONTROLLER_ENABLED="${EXTERNAL_CONTROLLER_ENABLED:-true}" -EXTERNAL_CONTROLLER="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" +if command -v systemctl >/dev/null 2>&1; then + CLASH_SERVICE_USER="$Service_User" CLASH_SERVICE_GROUP="$Service_Group" "$Install_Dir/scripts/install_systemd.sh" -ALLOW_INSECURE_TLS="${ALLOW_INSECURE_TLS:-false}" + if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then + systemctl enable "${Service_Name}.service" >/dev/null 2>&1 || true + fi + if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then + systemctl start "${Service_Name}.service" >/dev/null 2>&1 || true + fi -# 端口与配置工具 -# shellcheck disable=SC1090 -source "$Server_Dir/scripts/port_utils.sh" -CLASH_HTTP_PORT="$(resolve_port_value "HTTP" "$CLASH_HTTP_PORT")" -CLASH_SOCKS_PORT="$(resolve_port_value "SOCKS" "$CLASH_SOCKS_PORT")" -CLASH_REDIR_PORT="$(resolve_port_value "REDIR" "$CLASH_REDIR_PORT")" -EXTERNAL_CONTROLLER="$(resolve_host_port "External Controller" "$EXTERNAL_CONTROLLER" "127.0.0.1")" - -# shellcheck disable=SC1090 -source "$Server_Dir/scripts/config_utils.sh" - -#################### 函数定义 #################### - -# 自定义action函数,实现通用action功能(兼容 journald;关键错误会额外 echo 到 stderr) -success() { - echo -en "\033[60G[\033[1;32m OK \033[0;39m]\r" - return 0 -} - -failure() { - local rc=$? - echo -en "\033[60G[\033[1;31mFAILED\033[0;39m]\r" - [ -x /bin/plymouth ] && /bin/plymouth --details - return "$rc" -} - -action() { - local STRING - STRING=$1 - shift - - # 执行命令本身的成功/失败,不应让 UI 输出影响返回码 - if "$@"; then - success $"$STRING" || true - return 0 + if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then + Service_Enabled="enabled" else - failure $"$STRING" || true - return 1 - fi -} - -# 判断命令是否正常执行 -# - 手动模式:失败直接 exit -# - systemd 模式:只打印状态,不影响退出码 -if_success() { - local ok_msg=$1 - local fail_msg=$2 - local rc=$3 - - if [ "$rc" -eq 0 ]; then - action "$ok_msg" /bin/true || true - return 0 + Service_Enabled="disabled" fi - # rc != 0 - action "$fail_msg" /bin/false || true - - if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - # systemd 下不允许在 UI 函数中 exit - return "$rc" + if systemctl is-active --quiet "${Service_Name}.service" 2>/dev/null; then + Service_Started="active" else - exit "$rc" - fi -} - -ensure_subconverter() { - local bin="${Server_Dir}/tools/subconverter/subconverter" - local port="25500" - - # 没有二进制直接跳过 - if [ ! -x "$bin" ]; then - echo "[WARN] subconverter bin not found: $bin" - export SUBCONVERTER_READY="false" - return 0 - fi - - # 已在监听则认为就绪 - if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then - export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://127.0.0.1:${port}}" - export SUBCONVERTER_READY="true" - return 0 - fi - - # 启动(后台) - echo "[INFO] starting subconverter..." - (cd "${Server_Dir}/tools/subconverter" && nohup "./subconverter" >/dev/null 2>&1 &) - - # 等待端口起来 - for _ in 1 2 3 4 5; do - sleep 1 - if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then - export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://127.0.0.1:${port}}" - export SUBCONVERTER_READY="true" - echo "[OK] subconverter ready at ${SUBCONVERTER_URL}" - return 0 - fi - done - - echo "[WARN] subconverter start failed or port not ready" - export SUBCONVERTER_READY="false" - return 0 -} - -#################### 任务执行 #################### - -## 获取CPU架构信息 -# shellcheck disable=SC1090 -source "$Server_Dir/scripts/get_cpu_arch.sh" - -if [[ -z "${CpuArch:-}" ]]; then - echo "[ERROR] Failed to obtain CPU architecture" >&2 - exit 2 -fi - -# shellcheck disable=SC1090 -source "$Server_Dir/scripts/resolve_clash.sh" - -## 临时取消环境变量 -unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY || true - -######################################################## -# systemd 兜底:如果没有可用订阅 URL,则确保有 config.yaml -######################################################## -ensure_fallback_config() { - # conf/config.yaml 为空或不存在,则从 fallback 拷贝 - if [ ! -s "$Conf_Dir/config.yaml" ]; then - if [ -s "$Server_Dir/conf/fallback_config.yaml" ]; then - cp -f "$Server_Dir/conf/fallback_config.yaml" "$Conf_Dir/config.yaml" - echo -e "\033[33m[WARN]\033[0m 已复制 fallback_config.yaml -> conf/config.yaml(兜底)" - else - echo -e "\033[31m[ERROR]\033[0m 未找到可用的 conf/fallback_config.yaml,无法兜底启动" >&2 - if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - return 1 - else - exit 1 - fi - fi - fi - - # 强制写入真实 secret(失败时也遵循同样规则) - if ! force_write_secret "$Conf_Dir/config.yaml"; then - echo -e "\033[31m[ERROR]\033[0m 写入 secret 失败:$Conf_Dir/config.yaml" >&2 - if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - return 1 - else - exit 1 - fi - fi - - return 0 -} -SKIP_CONFIG_REBUILD=false - -# systemd 模式下若 URL 为空:直接兜底启动 -if [ "${SYSTEMD_MODE}" = "true" ] && [ -z "${URL:-}" ]; then - echo -e "\033[33m[WARN]\033[0m SYSTEMD_MODE=true 且 CLASH_URL 为空,跳过订阅更新,使用本地兜底配置启动" - ensure_fallback_config || true - SKIP_CONFIG_REBUILD=true -fi - -#################### Clash 订阅地址检测及配置文件下载 #################### -if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then - echo -e '\n正在检测订阅地址...' - Text1="Clash订阅地址可访问!" - Text2="Clash订阅地址不可访问!" - - CHECK_CMD=(curl -o /dev/null -L -sS --retry 5 -m 10 --connect-timeout 10 -w "%{http_code}") - if [ "$ALLOW_INSECURE_TLS" = "true" ]; then - CHECK_CMD+=(-k) - echo -e "\033[33m[WARN]\033[0m 已启用不安全的 TLS 下载(跳过证书校验)" - fi - if [ -n "${CLASH_HEADERS:-}" ]; then - CHECK_CMD+=(-H "$CLASH_HEADERS") - fi - CHECK_CMD+=("$URL") - - # 不让 set -e 干扰获取状态码 - set +e - status_code="$("${CHECK_CMD[@]}")" - curl_rc=$? - set -e - - # curl 本身失败,视为不可用 - if [ "$curl_rc" -ne 0 ]; then - status_code="" - ReturnStatus=1 - else - echo "$status_code" | grep -E '^[23][0-9]{2}$' &>/dev/null - ReturnStatus=$? - fi - - if [ "$ReturnStatus" -eq 0 ]; then - action "$Text1" /bin/true || true - else - if [ "$SYSTEMD_MODE" = "true" ]; then - action "$Text2(systemd 模式不退出,尝试使用旧配置/兜底配置)" /bin/false || true - echo -e "\033[33m[WARN]\033[0m Subscribe check failed: http_code=${status_code:-unknown}, url=${URL}" >&2 - ensure_fallback_config || true - SKIP_CONFIG_REBUILD=true - else - if_success "$Text1" "$Text2" "$ReturnStatus" - fi - fi -fi - -#################### 下载订阅并生成 config.yaml(非兜底路径) #################### -if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then - ensure_subconverter || true - echo -e '\n正在下载Clash配置文件...' - Text3="配置文件clash.yaml下载成功!" - Text4="配置文件clash.yaml下载失败!" - - # --- DBG: 显式打印并验证临时目录可写(systemd 下常见权限问题) --- - echo "[DBG] uid=$(id -u) user=$(id -un) SYSTEMD_MODE=${SYSTEMD_MODE:-}" - echo "[DBG] Server_Dir=$Server_Dir Conf_Dir=$Conf_Dir Temp_Dir=$Temp_Dir Log_Dir=$Log_Dir" - echo "[DBG] URL=$(printf '%q' "$URL")" - - mkdir -p "$Temp_Dir" 2>/dev/null || true - touch "$Temp_Dir/.write_test" 2>/dev/null || { echo "[ERR] Temp_Dir not writable: $Temp_Dir" >&2; exit 2; } - rm -f "$Temp_Dir/.write_test" 2>/dev/null || true - # --- DBG end --- - - CURL_CMD=(curl -fL -S --retry 2 --connect-timeout 10 -m 30 -o "$Temp_Dir/clash.yaml") - if [ "$ALLOW_INSECURE_TLS" = "true" ]; then - CURL_CMD+=(-k) - fi - if [ -n "${CLASH_HEADERS:-}" ]; then - CURL_CMD+=(-H "$CLASH_HEADERS") - fi - CURL_CMD+=("$URL") - - set +e - CURL_ERR="$Temp_Dir/curl.err" - : > "$CURL_ERR" - "${CURL_CMD[@]}" 2>>"$CURL_ERR" - ReturnStatus=$? - set -e - - echo "[DBG] curl rc=$ReturnStatus" - if [ -s "$CURL_ERR" ]; then - echo "[DBG] curl stderr (last 50 lines):" - tail -n 50 "$CURL_ERR" - fi - - if [ "$ReturnStatus" -ne 0 ]; then - WGET_CMD=(wget -q -O "$Temp_Dir/clash.yaml") - if [ "$ALLOW_INSECURE_TLS" = "true" ]; then - WGET_CMD+=(--no-check-certificate) - fi - if [ -n "${CLASH_HEADERS:-}" ]; then - WGET_CMD+=(--header="$CLASH_HEADERS") - fi - WGET_CMD+=("$URL") - - for _ in {1..10}; do - set +e - "${WGET_CMD[@]}" - ReturnStatus=$? - set -e - if [ "$ReturnStatus" -eq 0 ]; then - break - fi - done - fi - - CONFIG_FILE="${CONFIG_FILE:-$Temp_Dir/config.yaml}" - mkdir -p "$Temp_Dir" || true - - if [ "$ReturnStatus" -eq 0 ] && [ -s "$Temp_Dir/clash.yaml" ]; then - SRC_YAML="$Temp_Dir/clash.yaml" - - # 1) 判断是否是完整 Clash 配置(关键字段之一存在即可) - if grep -qE '^(proxies:|proxy-providers:|rules:|port:|mixed-port:|dns:)' "$SRC_YAML"; then - cp -f "$SRC_YAML" "$CONFIG_FILE" - echo "[INFO] subscription already is a full clash config" - else - # 2) 非完整配置:尝试用 subconverter 转换 - echo "[INFO] subscription is not a full config, try conversion via subconverter..." - - export IN_FILE="$SRC_YAML" - export OUT_FILE="$Temp_Dir/clash_converted.yaml" - - set +e - bash "$Server_Dir/scripts/clash_profile_conversion.sh" - conv_rc=$? - set -e - - if [ "$conv_rc" -eq 0 ] && [ -s "$OUT_FILE" ]; then - cp -f "$OUT_FILE" "$CONFIG_FILE" - echo "[INFO] conversion ok -> runtime config ready" - else - echo "[WARN] conversion skipped/failed, will keep original and rely on fallback" - cp -f "$SRC_YAML" "$CONFIG_FILE" - fi - fi - - # 3) 强制注入 external-controller / external-ui(运行态兜底) - force_write_controller_and_ui "$CONFIG_FILE" || true - - # 4) 强制注入 secret - force_write_secret "$CONFIG_FILE" || true - - # Optional: Fix test URLs to HTTPS for reliability (safe, narrow scope) - if [ "${FIX_TEST_URL_HTTPS:-true}" = "true" ] && [ -s "$CONFIG_FILE" ]; then - # 1) proxy-groups: url-test / fallback url - sed -i -E "s#(url:[[:space:]]*['\"])http://#\1https://#g" "$CONFIG_FILE" 2>/dev/null || true - - # 2) cfw-latency-url (some dashboards) - sed -i -E "s#(cfw-latency-url:[[:space:]]*['\"])http://#\1https://#g" "$CONFIG_FILE" 2>/dev/null || true - - # 3) proxy-providers health-check url (mihomo warns about this) - sed -i -E "s#(health-check:[[:space:]]*\n[[:space:]]*url:[[:space:]]*['\"])http://#\1https://#g" "$CONFIG_FILE" 2>/dev/null || true - fi - - # 5) 自检:失败则回退到旧配置(注意:脚本 set -e + trap ERR,必须 set +e 包裹) - BIN="${Server_Dir}/bin/clash-linux-amd64" - NEW_CFG="$CONFIG_FILE" - OLD_CFG="${Conf_Dir}/config.yaml" - TEST_OUT="$Temp_Dir/config.test.out" - - if [ -x "$BIN" ] && [ -f "$NEW_CFG" ]; then - # 先尝试自动修复 external-ui 的 SAFE_PATH 问题(内部会跑 -t) - set +e - fix_external_ui_by_safe_paths "$BIN" "$NEW_CFG" "$TEST_OUT" - test_rc=$? - set -e - - if [ "$test_rc" -ne 0 ]; then - echo "[ERROR] Generated config invalid, rc=$test_rc, reason(file=$TEST_OUT, size=$(wc -c <"$TEST_OUT" 2>/dev/null || echo 0))" >&2 - tail -n 120 "$TEST_OUT" >&2 || true - - echo "[ERROR] fallback to last good config: $OLD_CFG" >&2 - if [ -f "$OLD_CFG" ]; then - cp -f "$OLD_CFG" "$NEW_CFG" - else - echo "[FATAL] No valid config available, aborting startup" >&2 - exit 1 - fi - fi - fi - - echo "[INFO] Runtime config generated: $CONFIG_FILE (size=$(wc -c <"$CONFIG_FILE" 2>/dev/null || echo 0))" - else - echo "[WARN] Download did not produce clash.yaml (rc=$ReturnStatus), skip runtime config generation" >&2 - fi - - if [ "$ReturnStatus" -eq 0 ]; then - action "$Text3" /bin/true || true - else - if [ "$SYSTEMD_MODE" = "true" ]; then - action "$Text4(systemd 模式:下载失败,使用旧配置/兜底配置继续启动)" /bin/false || true - echo -e "\033[33m[WARN]\033[0m Download failed, will fallback. url=${URL}" >&2 - ensure_fallback_config || true - SKIP_CONFIG_REBUILD=true - else - if_success "$Text3" "$Text4(退出启动)" "$ReturnStatus" - fi - fi -fi - -# ========================================================= -# 判断订阅是否已是完整 Clash YAML(Meta / Mihomo / Premium) -# 若是完整配置,则直接使用,跳过后续代理拆解与拼接 -# ========================================================= -if grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$Temp_Dir/clash.yaml"; then - echo "[INFO] subscription is a full Clash config, use it directly" - cp -f "$Temp_Dir/clash.yaml" "$Conf_Dir/config.yaml" - - # 生成运行态(systemd non-root 实际启动用 Temp_Dir/config.yaml) - cp -f "$Temp_Dir/clash.yaml" "$Temp_Dir/config.yaml" - - # 写 controller/ui + secret(写到运行态) - force_write_controller_and_ui "$Temp_Dir/config.yaml" || true - force_write_secret "$Temp_Dir/config.yaml" || true - - # 同时把 conf/config.yaml 也补齐(方便你 grep/排查) - force_write_controller_and_ui "$Conf_Dir/config.yaml" || true - force_write_secret "$Conf_Dir/config.yaml" || true - - # 创建 UI 软链(systemd non-root 用 /tmp) - Dashboard_Src="$Server_Dir/dashboard/public" - if [ -d "$Dashboard_Src" ]; then - ln -sfn "$Dashboard_Src" "$Conf_Dir/ui" 2>/dev/null || true - fi - - SKIP_CONFIG_REBUILD=true - fi - -#################### 订阅转换/拼接(非兜底路径) #################### -if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then - # 运行期配置文件:默认用 Temp_Dir(systemd + clash 用户可写) - CONFIG_FILE="$Temp_Dir/config.yaml" - - # 1) 重命名订阅文件 - \cp -a "$Temp_Dir/clash.yaml" "$Temp_Dir/clash_config.yaml" - - # 2) 判断订阅内容是否符合 clash 配置文件标准,尝试转换(需 subconverter) - # shellcheck disable=SC1090 - source "$Server_Dir/scripts/resolve_subconverter.sh" - - if [ "${Subconverter_Ready:-false}" = "true" ]; then - echo -e '\n判断订阅内容是否符合clash配置文件标准:' - export SUBCONVERTER_BIN="$Subconverter_Bin" - bash "$Server_Dir/scripts/clash_profile_conversion.sh" - sleep 1 - else - echo -e "\033[33m[WARN]\033[0m 未检测到可用的 subconverter,跳过订阅转换" - fi - - # 3) 订阅形态判断: - # - 如果已经是完整 Clash 配置(Meta/Mihomo 常见 mixed-port / proxy-providers 等),直接用它作为运行配置 - # - 否则才走 “proxies: 抽取 + template 拼接” - if grep -qE '^(mixed-port:|port:|proxy-providers:|proxies:)' "$Temp_Dir/clash_config.yaml"; then - # 情况 A:完整配置(优先) - if grep -q '^proxies:' "$Temp_Dir/clash_config.yaml" || grep -q '^proxy-providers:' "$Temp_Dir/clash_config.yaml" || grep -q '^mixed-port:' "$Temp_Dir/clash_config.yaml" || grep -q '^port:' "$Temp_Dir/clash_config.yaml"; then - echo "[INFO] subscription looks like a full Clash config, use it directly" - cp -f "$Temp_Dir/clash_config.yaml" "$CONFIG_FILE" - # 写入 secret(运行态) - force_write_secret "$CONFIG_FILE" - # 直接跳过后续拼接流程 - SKIP_CONFIG_REBUILD=true - fi - fi - - # 情况 B:不是完整配置,才尝试抽取 proxies 并拼接 - if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then - if grep -q '^proxies:' "$Temp_Dir/clash_config.yaml"; then - sed -n '/^proxies:/,$p' "$Temp_Dir/clash_config.yaml" > "$Temp_Dir/proxy.txt" - else - echo "[ERROR] subscription is not a full config and also has no 'proxies:'; cannot build config." >&2 - # systemd 模式:兜底继续;非 systemd:退出 - if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - ensure_fallback_config || true - SKIP_CONFIG_REBUILD=true - else - exit 2 - fi - fi - fi - - # 4) 合并形成新的 config,并替换配置占位符 - if [ "$SKIP_CONFIG_REBUILD" != "true" ]; then - cat "$Temp_Dir/templete_config.yaml" > "$CONFIG_FILE" - cat "$Temp_Dir/proxy.txt" >> "$CONFIG_FILE" - - sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$CONFIG_FILE" - sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$CONFIG_FILE" - sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$CONFIG_FILE" - sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$CONFIG_FILE" - sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$CONFIG_FILE" - fi - - # 5) 配置 external-controller - if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then - sed -i "s/EXTERNAL_CONTROLLER_PLACEHOLDER/${EXTERNAL_CONTROLLER}/g" "$CONFIG_FILE" - else - sed -i "s/external-controller: 'EXTERNAL_CONTROLLER_PLACEHOLDER'/# external-controller: disabled/g" "$CONFIG_FILE" - fi - - apply_tun_config "$CONFIG_FILE" - apply_mixin_config "$CONFIG_FILE" "$Server_Dir" - - # 6) 是否同步到 conf(root/非 systemd 时才做;systemd+非root跳过) - \cp "$CONFIG_FILE" "$Conf_Dir/" - - # 7) Dashboard external-ui(systemd+非root:把 ui 放 Temp_Dir 下,避免写 conf) - Work_Dir="$(cd "$(dirname "$0")" && pwd)" - Dashboard_Src="${Work_Dir}/dashboard/public" - - if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then - if [ "${SYSTEMD_MODE:-false}" = "true" ] && [ "$(id -u)" -ne 0 ]; then - # runtime ui path (writable) - Dashboard_Link="$Temp_Dir/ui" - if [ -d "$Dashboard_Src" ]; then - ln -sfn "$Dashboard_Src" "$Dashboard_Link" 2>/dev/null || true - fi - else - # conf ui path (root can manage) - Dashboard_Link="${Conf_Dir}/ui" - if [ -d "$Dashboard_Src" ]; then - ln -sfn "$Dashboard_Src" "$Dashboard_Link" || true - else - echo -e "\033[33m[WARN]\033[0m Dashboard source not found: $Dashboard_Src (external-ui may not work)" - fi - fi - - # ensure external-ui points to Dashboard_Link - if grep -qE '^[[:space:]]*external-ui:' "$CONFIG_FILE"; then - sed -i -E "s|^[[:space:]]*external-ui:.*$|external-ui: ${Dashboard_Link}|g" "$CONFIG_FILE" - else - printf "\nexternal-ui: %s\n" "$Dashboard_Link" >> "$CONFIG_FILE" - fi - fi - - # 8) 写入 secret(写到 runtime config) - force_write_secret "$CONFIG_FILE" - -else - # 兜底路径:尽量也写入 secret(conf/config.yaml 可写时) - if grep -qE '^secret:\s*' "$Conf_Dir/config.yaml" 2>/dev/null; then - force_write_secret "$Conf_Dir/config.yaml" - else - echo "secret: ${Secret}" >> "$Conf_Dir/config.yaml" || true - fi -fi - -#################### 启动Clash服务 #################### - -# 选择运行期配置文件与工作目录 -CONFIG_FILE="${CONFIG_FILE:-$Conf_Dir/config.yaml}" -RUNTIME_DIR="${Conf_Dir}" - -# 启动前确保配置文件存在且非空 -if [ ! -s "$CONFIG_FILE" ]; then - echo -e "\033[31m[ERROR]\033[0m config 不存在或为空:$CONFIG_FILE,无法启动 Clash" >&2 - exit 2 -fi - -# 最终护栏:禁止未渲染的占位符进入运行态 -if grep -q '\${' "$CONFIG_FILE"; then - echo "[ERROR] config contains unresolved placeholders (\${...}): $CONFIG_FILE" >&2 - exit 2 -fi - -# 确保运行目录存在且可写(clash/mihomo 可能会写 cache/geo 数据) -mkdir -p "$RUNTIME_DIR" 2>/dev/null || true -touch "$RUNTIME_DIR/.write_test" 2>/dev/null || { - echo "[ERROR] runtime dir not writable: $RUNTIME_DIR (uid=$(id -u))" >&2 - exit 2 -} -rm -f "$RUNTIME_DIR/.write_test" 2>/dev/null || true - -echo -e '\n正在启动Clash服务...' -Text5="服务启动成功!" -Text6="服务启动失败!" - -Clash_Bin="$(resolve_clash_bin "$Server_Dir" "$CpuArch")" -ReturnStatus=$? - -if [ "$ReturnStatus" -eq 0 ]; then - if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - echo "[INFO] SYSTEMD_MODE=true,前台启动交给 systemd 监管" - echo "[INFO] Using config: $CONFIG_FILE" - echo "[INFO] Using runtime dir: $RUNTIME_DIR" - - # systemd 前台:只用 -f 指定配置文件,-d 作为工作目录 - exec "$Clash_Bin" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" - else - echo "[INFO] 后台启动 (nohup)" - echo "[INFO] Using config: $CONFIG_FILE" - echo "[INFO] Using runtime dir: $RUNTIME_DIR" - - nohup "$Clash_Bin" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$Log_Dir/clash.log" 2>&1 & - PID=$! - ReturnStatus=$? - - if [ "$ReturnStatus" -eq 0 ]; then - echo "$PID" > "$PID_FILE" - fi - fi -fi - -if [ "${SYSTEMD_MODE:-false}" = "true" ]; then - if_success "$Text5" "$Text6" "$ReturnStatus" || true -else - if_success "$Text5" "$Text6" "$ReturnStatus" -fi - -#################### 输出信息 #################### - -echo '' -if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then - echo -e "Clash Dashboard 访问地址: http://${EXTERNAL_CONTROLLER}/ui" - - SHOW_SECRET="${CLASH_SHOW_SECRET:-false}" - SHOW_SECRET_MASKED="${CLASH_SHOW_SECRET_MASKED:-true}" - - if [ "$SHOW_SECRET" = "true" ]; then - echo -e "Secret: ${Secret}" - elif [ "$SHOW_SECRET_MASKED" = "true" ]; then - # 脱敏:前4后4 - masked="${Secret:0:4}****${Secret: -4}" - echo -e "Secret: ${masked} (set CLASH_SHOW_SECRET=true to show full)" - else - echo -e "Secret: 已生成(未显示)。查看:/opt/clash-for-linux/conf/config.yaml 或 .env" + Service_Started="inactive" fi else - echo -e "External Controller (Dashboard) 已禁用" + warn "未检测到 systemd,已跳过服务单元生成" fi -echo '' -#################### 写入代理环境变量文件 #################### +# ========================= +# Shell 代理快捷命令 +# 生成:/etc/profile.d/clash-for-linux.sh +# ========================= +PROFILED_FILE="/etc/profile.d/clash-for-linux.sh" -Env_File="${CLASH_ENV_FILE:-}" +install_profiled() { + local http_port="${MIXED_PORT:-7890}" + # 兼容你后面可能支持 auto:auto 就先用 7890 + [ "$http_port" = "auto" ] && http_port="7890" -if [ "$Env_File" = "off" ] || [ "$Env_File" = "disabled" ]; then - echo -e "\033[33m[WARN]\033[0m 已关闭环境变量文件生成" -else - if [ -z "$Env_File" ]; then - if [ -w /etc/profile.d ]; then - Env_File="/etc/profile.d/clash-for-linux.sh" - else - Env_File="$Temp_Dir/clash-for-linux.sh" - fi - fi + # 只写 IPv4 loopback,避免某些环境 ::1 解析问题 + sudo tee "$PROFILED_FILE" >/dev/null <"$Env_File"<}" + echo "https_proxy=\${https_proxy:-}" + echo "all_proxy=\${all_proxy:-}" } EOF - echo -e "请执行以下命令加载环境变量: source ${Env_File}\n" - echo -e "请执行以下命令开启系统代理: proxy_on\n" - echo -e "若要临时关闭系统代理,请执行: proxy_off\n" + sudo chmod 644 "$PROFILED_FILE" +} + +install_profiled || true + +# ========================= +# 安装 clashctl 命令 +# ========================= +if [ -f "$Install_Dir/clashctl" ]; then + install -m 0755 "$Install_Dir/clashctl" /usr/local/bin/clashctl fi + +# ========================= +# 友好收尾输出(闭环) +# ========================= + +section "安装完成" +ok "Clash for Linux 已安装至: $(path "${Install_Dir}")" + +log "📦 安装目录:$(path "${Install_Dir}")" +log "👤 运行用户:${Service_User}:${Service_Group}" +log "🔧 服务名称:${Service_Name}.service" + +if command -v systemctl >/dev/null 2>&1; then + section "服务状态" + + se="${Service_Enabled:-unknown}" + ss="${Service_Started:-unknown}" + + [[ "$se" == "enabled" ]] && se_colored="$(good "$se")" || se_colored="$(bad "$se")" + [[ "$ss" == "active" ]] && ss_colored="$(good "$ss")" || ss_colored="$(bad "$ss")" + + log "🧷 开机自启:${se_colored}" + log "🟢 服务状态:${ss_colored}" + + log "" + log "${C_BOLD}常用命令:${C_NC}" + log " $(cmd "sudo systemctl status ${Service_Name}.service")" + log " $(cmd "sudo systemctl restart ${Service_Name}.service")" +fi + +# ========================= +# Dashboard / Secret +# ========================= +section "控制面板" + +api_port="$(parse_port "${EXTERNAL_CONTROLLER}")" +api_host="${EXTERNAL_CONTROLLER%:*}" + +if [[ -z "$api_host" ]] || [[ "$api_host" == "$EXTERNAL_CONTROLLER" ]]; then + api_host="127.0.0.1" +fi + +CONF_DIR="$Install_Dir/conf" +CONF_FILE="$CONF_DIR/config.yaml" + +SECRET_VAL="" +if wait_secret_ready "$CONF_FILE" 6; then + SECRET_VAL="$(read_secret_from_config "$CONF_FILE" || true)" +fi + +dash="http://${api_host}:${api_port}/ui" +log "🌐 Dashboard:$(url "$dash")" + +if [[ -n "$SECRET_VAL" ]]; then + MASKED="${SECRET_VAL}" + log "🔐 Secret:${C_YELLOW}${MASKED}${C_NC}" + log " 查看完整 Secret:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_FILE\" | head -n 1")" +else + log "🔐 Secret:${C_YELLOW}启动中暂未读到(稍后再试)${C_NC}" + log " 稍后查看:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_FILE\" | head -n 1")" +fi + +# ========================= +# 订阅配置(必须) +# ========================= +section "订阅状态" + +ENV_FILE="${Install_Dir}/.env" + +if [[ -n "${CLASH_URL:-}" ]]; then + ok "订阅地址已配置(CLASH_URL 已写入 .env)" +else + warn "订阅地址未配置(必须)" + log "" + log "配置订阅地址:" + log " $(cmd "sudo bash -c 'echo \"CLASH_URL=<订阅地址>\" > ${ENV_FILE}'")" + log "" + log "配置完成后重启服务:" + log " $(cmd "sudo systemctl restart ${Service_Name}.service")" +fi + +# ========================= +# 下一步 +# ========================= +section "下一步开启代理(可选)" + +PROFILED_FILE="/etc/profile.d/clash-for-linux.sh" + +if [ -f "$PROFILED_FILE" ]; then + log " $(cmd "source $PROFILED_FILE")" + log " $(cmd "proxy_on")" +else + log " (未安装 Shell 代理快捷命令,跳过)" +fi + +# ========================= +# 启动后快速诊断 +# ========================= +sleep 1 +if command -v journalctl >/dev/null 2>&1; then + if journalctl -u "${Service_Name}.service" -n 50 --no-pager 2>/dev/null \ + | grep -q "Clash订阅地址不可访问"; then + warn "服务启动异常:订阅不可用,请检查 CLASH_URL(可能过期 / 404 / 被墙)。" + fi +fi \ No newline at end of file