diff --git a/.env b/.env index 90a360d..ba52ac7 100644 --- a/.env +++ b/.env @@ -42,7 +42,7 @@ CLASH_SHOW_SECRET_MASKED=true # - 默认仅监听本机:127.0.0.1:9090 (推荐) # - 如需局域网访问再改成:0.0.0.0:9090,并确保 CLASH_SECRET 足够复杂 export EXTERNAL_CONTROLLER_ENABLED=true -export EXTERNAL_CONTROLLER='127.0.0.1:9090' +export EXTERNAL_CONTROLLER='0.0.0.0:9090' # ------------------------- # 3) 代理端口与监听(常用) diff --git a/install.sh b/install.sh index 3b7aef9..42de59b 100755 --- a/install.sh +++ b/install.sh @@ -5,7 +5,7 @@ set -euo pipefail # 基础参数 # ========================= Server_Dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -Install_Dir="${CLASH_INSTALL_DIR:-/opt/clash-for-linux}" +Install_Dir="${CLASH_INSTALL_DIR:-$Server_Dir}" Service_Name="clash-for-linux" Service_User="root" Service_Group="root" @@ -136,7 +136,6 @@ if [[ -z "${CpuArch:-}" ]]; then err "无法识别 CPU 架构" exit 1 fi -info "CPU architecture: ${CpuArch}" # ========================= # .env 写入工具:write_env_kv(必须在 prompt 之前定义) @@ -351,57 +350,35 @@ read_secret_from_config() { printf '%s' "$s" } -# 判断 systemd 是否可用(仅有 systemctl 命令但 PID 1 不是 systemd 时视为不可用) -systemd_ready() { - command -v systemctl >/dev/null 2>&1 || return 1 - systemctl show --property=Version --value >/dev/null 2>&1 || return 1 - return 0 -} - # ========================= # systemd 安装与启动 # ========================= Service_Enabled="unknown" Service_Started="unknown" -Systemd_Usable="false" -if systemd_ready; then - Systemd_Usable="true" -fi +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" -if [ "$Systemd_Usable" = "true" ]; then - if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ] || [ "${CLASH_START_SERVICE:-true}" = "true" ]; then - CLASH_SERVICE_USER="$Service_User" CLASH_SERVICE_GROUP="$Service_Group" "$Install_Dir/scripts/install_systemd.sh" + if [ "${CLASH_ENABLE_SERVICE:-true}" = "true" ]; then + systemctl start "${Service_Name}.service" || true + fi + if [ "${CLASH_START_SERVICE:-true}" = "true" ]; then + systemctl start "${Service_Name}.service" || true + fi - 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 - - if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then - Service_Enabled="enabled" - else - Service_Enabled="disabled" - fi - - if systemctl is-active --quiet "${Service_Name}.service" 2>/dev/null; then - Service_Started="active" - else - Service_Started="inactive" - fi + if systemctl is-enabled --quiet "${Service_Name}.service" 2>/dev/null; then + Service_Enabled="enabled" else - info "已按配置跳过 systemd 服务安装与启动(CLASH_ENABLE_SERVICE=false 且 CLASH_START_SERVICE=false)" Service_Enabled="disabled" + fi + + if systemctl is-active --quiet "${Service_Name}.service" 2>/dev/null; then + Service_Started="active" + else Service_Started="inactive" fi else - if command -v systemctl >/dev/null 2>&1; then - warn "检测到 systemctl 命令,但当前环境不可用 systemd(常见于 Docker 容器),已跳过服务单元生成" - else - warn "未检测到 systemd,已跳过服务单元生成" - fi + warn "未检测到 systemd,已跳过服务单元生成" fi # ========================= @@ -416,7 +393,7 @@ install_profiled() { [ "$http_port" = "auto" ] && http_port="7890" # 只写 IPv4 loopback,避免某些环境 ::1 解析问题 - tee "$PROFILED_FILE" >/dev/null </dev/null </dev/null 2>&1; then section "服务状态" se="${Service_Enabled:-unknown}" @@ -486,11 +463,6 @@ if [ "$Systemd_Usable" = "true" ]; then log "${C_BOLD}常用命令:${C_NC}" log " $(cmd "sudo systemctl status ${Service_Name}.service")" log " $(cmd "sudo systemctl restart ${Service_Name}.service")" -else - section "服务状态" - warn "当前环境未启用 systemd(如 Docker 容器),请使用 clashctl 管理进程" - log " $(cmd "sudo clashctl start")" - log " $(cmd "sudo clashctl restart")" fi # ========================= @@ -501,28 +473,65 @@ section "控制面板" api_port="$(parse_port "${EXTERNAL_CONTROLLER}")" api_host="${EXTERNAL_CONTROLLER%:*}" +get_public_ip() { + if command -v curl >/dev/null 2>&1; then + curl -4 -fsS --max-time 3 https://api.ipify.org 2>/dev/null \ + || curl -4 -fsS --max-time 3 https://ifconfig.me 2>/dev/null \ + || curl -4 -fsS --max-time 3 https://ipv4.icanhazip.com 2>/dev/null \ + || true + elif command -v wget >/dev/null 2>&1; then + wget -qO- --timeout=3 https://api.ipify.org 2>/dev/null \ + || wget -qO- --timeout=3 https://ifconfig.me 2>/dev/null \ + || wget -qO- --timeout=3 https://ipv4.icanhazip.com 2>/dev/null \ + || true + else + true + fi +} + if [[ -z "$api_host" ]] || [[ "$api_host" == "$EXTERNAL_CONTROLLER" ]]; then api_host="127.0.0.1" fi +if [[ "$api_host" == "0.0.0.0" ]] || [[ "$api_host" == "::" ]] || [[ "$api_host" == "localhost" ]]; then + api_host="$(get_public_ip | tr -d '\r\n')" + [[ -z "$api_host" ]] && api_host="$(hostname -I 2>/dev/null | awk '{print $1}')" + [[ -z "$api_host" ]] && api_host="127.0.0.1" +fi + CONF_DIR="$Install_Dir/conf" -CONF_FILE="$CONF_DIR/config.yaml" +TEMP_DIR="$Install_Dir/temp" SECRET_VAL="" -if wait_secret_ready "$CONF_FILE" 6; then - SECRET_VAL="$(read_secret_from_config "$CONF_FILE" || true)" -fi +SECRET_FILE="" + +for _ in {1..15}; do + for f in \ + "$TEMP_DIR/config.yaml" \ + "$CONF_DIR/config.yaml" + do + SECRET_VAL="$(read_secret_from_config "$f" 2>/dev/null || true)" + if [[ -n "$SECRET_VAL" ]]; then + SECRET_FILE="$f" + break 2 + fi + done + sleep 0.2 +done dash="http://${api_host}:${api_port}/ui" log "🌐 Dashboard:$(url "$dash")" +SHOW_FILE="${SECRET_FILE:-$CONF_DIR/config.yaml}" + if [[ -n "$SECRET_VAL" ]]; then - MASKED="${SECRET_VAL:0:4}****${SECRET_VAL: -4}" + 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")" + # log " 查看完整 Secret:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$SHOW_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")" + log " 稍后查看:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$CONF_DIR/config.yaml\" | head -n 1")" + log " 也可检查运行态:$(cmd "sudo sed -nE 's/^[[:space:]]*secret:[[:space:]]*//p' \"$TEMP_DIR/config.yaml\" | head -n 1")" fi # ========================= @@ -541,11 +550,7 @@ else log " $(cmd "sudo bash -c 'echo \"CLASH_URL=<订阅地址>\" > ${ENV_FILE}'")" log "" log "配置完成后重启服务:" - if [ "$Systemd_Usable" = "true" ]; then - log " $(cmd "sudo systemctl restart ${Service_Name}.service")" - else - log " $(cmd "sudo clashctl restart")" - fi + log " $(cmd "sudo systemctl restart ${Service_Name}.service")" fi # ========================= @@ -566,9 +571,9 @@ fi # 启动后快速诊断 # ========================= sleep 1 -if [ "$Systemd_Usable" = "true" ] && command -v journalctl >/dev/null 2>&1; then +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 +fi \ No newline at end of file diff --git a/scripts/get_cpu_arch.sh b/scripts/get_cpu_arch.sh index 65d677f..a9a98e2 100644 --- a/scripts/get_cpu_arch.sh +++ b/scripts/get_cpu_arch.sh @@ -47,4 +47,5 @@ else exitWithError "Unsupported Linux distribution" fi -echo "CPU architecture: $CpuArch" +log_info() { echo "[INFO] $*"; } +log_info "CPU architecture: $CpuArch" \ No newline at end of file diff --git a/scripts/install_systemd.sh b/scripts/install_systemd.sh index 04b99e9..132e486 100755 --- a/scripts/install_systemd.sh +++ b/scripts/install_systemd.sh @@ -10,47 +10,64 @@ Service_User="root" Service_Group="root" Unit_Path="/etc/systemd/system/${Service_Name}.service" -PID_FILE="$Server_Dir/temp/clash.pid" +Env_File="$Server_Dir/temp/clash-for-linux.sh" #################### 权限检查 #################### if [ "$(id -u)" -ne 0 ]; then - echo -e "\033[31m[ERROR] 需要 root 权限来安装 systemd 单元\033[0m" + echo -e "[ERROR] 需要 root 权限来安装 systemd 单元" exit 1 fi #################### 目录初始化 #################### -install -d -m 0755 \ - "$Server_Dir/conf" \ - "$Server_Dir/logs" \ - "$Server_Dir/temp" +install -d -m 0755 "$Server_Dir/conf" "$Server_Dir/logs" "$Server_Dir/temp" + +# 预创建 env 文件,避免 systemd 因路径不存在报错 +: > "$Env_File" +chmod 0644 "$Env_File" #################### 生成 systemd Unit #################### -cat >"$Unit_Path"<"$Unit_Path" </dev/null 2>&1 || true -echo -e "\033[32m[OK] 已生成 systemd 单元: ${Unit_Path}\033[0m" -echo -e "可执行以下命令启动服务:" -echo -e " sudo systemctl enable --now ${Service_Name}.service" +echo -e "[OK] 已生成 systemd 单元: ${Unit_Path}" +echo -e "已启用开机自启,可执行以下命令启动服务:" +echo -e " systemctl restart ${Service_Name}.service" +echo -e "查看状态:" +echo -e " systemctl status ${Service_Name}.service -l --no-pager" diff --git a/start.sh b/start.sh index 9bd352d..f2be2b1 100644 --- a/start.sh +++ b/start.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # 严格模式 -set -eo pipefail +set -euo pipefail # --- DEBUG: 打印具体失败的行号和命令(systemd 下非常关键) --- trap 'rc=$?; echo "[ERR] rc=$rc line=$LINENO cmd=$BASH_COMMAND" >&2' ERR @@ -90,14 +90,37 @@ URL="${CLASH_URL:-}" URL="$(printf '%s' "$URL" | tr -d '\r')" URL="$(printf '%s' "$URL" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" +# 允许手动启动时交互填写;直接回车则切到本地兜底配置 +MANUAL_EMPTY_URL_FALLBACK=false +if [ -z "$URL" ] && [ "${SYSTEMD_MODE:-false}" != "true" ]; then + if [ -t 0 ]; then + echo + echo "[WARN] 未检测到订阅地址(CLASH_URL 为空)" + echo "请粘贴你的 Clash 订阅地址(直接回车将使用本地兜底配置启动):" + read -r -p "Clash URL: " input_url + input_url="$(printf '%s' "$input_url" | tr -d '\r')" + input_url="$(printf '%s' "$input_url" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')" + + if [ -n "$input_url" ]; then + if ! printf '%s' "$input_url" | grep -Eq '^https?://'; then + echo "[ERR] CLASH_URL 格式无效:必须以 http:// 或 https:// 开头" >&2 + exit 2 + fi + URL="$input_url" + export CLASH_URL="$URL" + else + echo "[WARN] 未填写订阅地址,切换为本地兜底配置启动" + MANUAL_EMPTY_URL_FALLBACK=true + fi + else + echo "[ERR] CLASH_URL 为空(未配置订阅地址)" >&2 + exit 2 + fi +fi + #让 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 @@ -119,10 +142,10 @@ fi # 兜底生成随机 secret if [ -z "$Secret" ]; then if command -v openssl >/dev/null 2>&1; then - Secret="$(openssl rand -hex 32)" + Secret="$(openssl rand -hex 16)" else # 32 bytes -> 64 hex chars - Secret="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')" + Secret="$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')" fi fi @@ -179,7 +202,7 @@ ensure_ui_links() { force_write_controller_and_ui() { local file="$1" - local controller="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" + local controller="${EXTERNAL_CONTROLLER:-0.0.0.0:9090}" [ -n "$file" ] || return 1 @@ -249,11 +272,11 @@ fix_external_ui_by_safe_paths() { 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_LISTEN_IP="${CLASH_LISTEN_IP:-0.0.0.0}" CLASH_ALLOW_LAN="${CLASH_ALLOW_LAN:-false}" EXTERNAL_CONTROLLER_ENABLED="${EXTERNAL_CONTROLLER_ENABLED:-true}" -EXTERNAL_CONTROLLER="${EXTERNAL_CONTROLLER:-127.0.0.1:9090}" +EXTERNAL_CONTROLLER="${EXTERNAL_CONTROLLER:-0.0.0.0:9090}" ALLOW_INSECURE_TLS="${ALLOW_INSECURE_TLS:-false}" @@ -326,6 +349,10 @@ ensure_subconverter() { local bin="${Server_Dir}/tools/subconverter/subconverter" local port="25500" + # 自动获取服务器IP + local host_ip + host_ip="$(hostname -I | awk '{print $1}')" + # 没有二进制直接跳过 if [ ! -x "$bin" ]; then echo "[WARN] subconverter bin not found: $bin" @@ -335,20 +362,20 @@ ensure_subconverter() { # 已在监听则认为就绪 if ss -lntp 2>/dev/null | grep -qE ":${port}[[:space:]]"; then - export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://127.0.0.1:${port}}" + export SUBCONVERTER_URL="${SUBCONVERTER_URL:-http://${host_ip}:${port}}" export SUBCONVERTER_READY="true" return 0 fi - # 启动(后台) + # 启动(监听所有IP) echo "[INFO] starting subconverter..." - (cd "${Server_Dir}/tools/subconverter" && nohup "./subconverter" >/dev/null 2>&1 &) + (cd "${Server_Dir}/tools/subconverter" && nohup "./subconverter" -listen 0.0.0.0:${port} >/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_URL="${SUBCONVERTER_URL:-http://${host_ip}:${port}}" export SUBCONVERTER_READY="true" echo "[OK] subconverter ready at ${SUBCONVERTER_URL}" return 0 @@ -410,9 +437,13 @@ ensure_fallback_config() { } 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 为空,跳过订阅更新,使用本地兜底配置启动" +# systemd 模式下 URL 为空,或手动模式下用户回车跳过:直接兜底启动 +if { [ "${SYSTEMD_MODE}" = "true" ] && [ -z "${URL:-}" ]; } || [ "${MANUAL_EMPTY_URL_FALLBACK:-false}" = "true" ]; then + if [ "${SYSTEMD_MODE}" = "true" ]; then + echo -e "\033[33m[WARN]\033[0m SYSTEMD_MODE=true 且 CLASH_URL 为空,跳过订阅更新,使用本地兜底配置启动" + else + echo -e "\033[33m[WARN]\033[0m 手动模式未填写订阅地址,跳过订阅更新,使用本地兜底配置启动" + fi ensure_fallback_config || true SKIP_CONFIG_REBUILD=true fi @@ -797,12 +828,30 @@ Clash_Bin="$(resolve_clash_bin "$Server_Dir" "$CpuArch")" ReturnStatus=$? if [ "$ReturnStatus" -eq 0 ]; then + 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 + masked="${Secret:0:4}****${Secret: -4}" + echo -e "Secret: ${masked} (set CLASH_SHOW_SECRET=true to show full)" + else + echo -e "Secret: 已生成(未显示)。查看:${CONFIG_FILE} 或 .env" + fi + else + echo -e "External Controller (Dashboard) 已禁用" + fi + echo '' + 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)" @@ -825,29 +874,6 @@ 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" - fi -else - echo -e "External Controller (Dashboard) 已禁用" -fi -echo '' - #################### 写入代理环境变量文件 #################### Env_File="${CLASH_ENV_FILE:-}" diff --git a/uninstall.sh b/uninstall.sh index 117944f..2ae7030 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,142 +1,152 @@ #!/usr/bin/env bash set -euo pipefail -# ========================= -# 参数(对标 install.sh + install_systemd.sh) -# ========================= -Install_Dir="${CLASH_INSTALL_DIR:-/opt/clash-for-linux}" -Service_Name="clash-for-linux" -Service_User="root" -Service_Group="root" -Unit_Path="/etc/systemd/system/${Service_Name}.service" +# More accurate uninstall for clash-for-linux +SERVICE_NAME="clash-for-linux" +UNIT_PATH="/etc/systemd/system/${SERVICE_NAME}.service" -# ========================= -# 彩色输出 -# ========================= RED='\033[31m' GREEN='\033[32m' YELLOW='\033[33m' NC='\033[0m' - info() { echo -e "${GREEN}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } err() { echo -e "${RED}[ERROR]${NC} $*"; } -# ========================= -# 前置校验 -# ========================= if [ "$(id -u)" -ne 0 ]; then err "需要 root 权限执行卸载脚本(请使用 sudo bash uninstall.sh)" exit 1 fi -info "开始卸载 ${Service_Name} ..." -info "Install_Dir=${Install_Dir}" +# Candidate install dirs: +# 1) explicit CLASH_INSTALL_DIR +# 2) working directory if it looks like clash-for-linux +# 3) service WorkingDirectory / ExecStart path inferred from unit +# 4) common defaults +candidates=() +[ -n "${CLASH_INSTALL_DIR:-}" ] && candidates+=("${CLASH_INSTALL_DIR}") +PWD_BASENAME="$(basename "${PWD}")" +if [ "$PWD_BASENAME" = "clash-for-linux" ] && [ -f "${PWD}/start.sh" ]; then + candidates+=("${PWD}") +fi -# ========================= -# 1) 优雅停止(优先 shutdown.sh,再 systemd) -# ========================= -if [ -f "${Install_Dir}/shutdown.sh" ]; then +if [ -f "$UNIT_PATH" ]; then + wd="$(sed -nE 's#^WorkingDirectory=(.*)#\1#p' "$UNIT_PATH" | head -n1 || true)" + [ -n "$wd" ] && candidates+=("$wd") + + exec_path="$(sed -nE 's#^ExecStart=/bin/bash[[:space:]]+([^[:space:]]+/start\.sh).*#\1#p' "$UNIT_PATH" | head -n1 || true)" + if [ -n "$exec_path" ]; then + candidates+=("$(dirname "$exec_path")") + fi +fi + +candidates+=("/root/clash-for-linux" "/opt/clash-for-linux") + +# normalize + uniq + choose first existing dir containing start.sh or shutdown.sh +INSTALL_DIR="" +declare -A seen +for d in "${candidates[@]}"; do + [ -n "$d" ] || continue + d="${d%/}" + [ -n "$d" ] || continue + if [ -z "${seen[$d]:-}" ]; then + seen[$d]=1 + if [ -d "$d" ] && { [ -f "$d/start.sh" ] || [ -f "$d/shutdown.sh" ] || [ -d "$d/conf" ]; }; then + INSTALL_DIR="$d" + break + fi + fi +done + +if [ -z "$INSTALL_DIR" ]; then + warn "未能自动识别安装目录,将按候选路径继续清理 systemd / 环境文件。" +else + info "识别到安装目录: $INSTALL_DIR" +fi + +info "开始卸载 ${SERVICE_NAME} ..." + +# 1) graceful stop +if [ -n "$INSTALL_DIR" ] && [ -f "${INSTALL_DIR}/shutdown.sh" ]; then info "执行 shutdown.sh(优雅停止)..." - bash "${Install_Dir}/shutdown.sh" >/dev/null 2>&1 || true + bash "${INSTALL_DIR}/shutdown.sh" >/dev/null 2>&1 || true fi if command -v systemctl >/dev/null 2>&1; then info "停止并禁用 systemd 服务..." - systemctl stop "${Service_Name}.service" >/dev/null 2>&1 || true - systemctl disable "${Service_Name}.service" >/dev/null 2>&1 || true + systemctl stop "${SERVICE_NAME}.service" >/dev/null 2>&1 || true + systemctl disable "${SERVICE_NAME}.service" >/dev/null 2>&1 || true fi -# ========================= -# 2) 兜底:按 PID 文件杀进程(对标 unit 的 PIDFile) -# ========================= -PID_FILE="${Install_Dir}/temp/clash.pid" -if [ -f "$PID_FILE" ]; then - PID="$(cat "$PID_FILE" 2>/dev/null || true)" - if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then - info "检测到 PID=${PID},尝试停止..." - kill "$PID" 2>/dev/null || true - sleep 1 - if kill -0 "$PID" 2>/dev/null; then - warn "进程仍在运行,强制 kill -9 ${PID}" - kill -9 "$PID" 2>/dev/null || true +# 2) stop process by pid file from all likely dirs +for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do + [ -n "$d" ] || continue + PID_FILE="$d/temp/clash.pid" + if [ -f "$PID_FILE" ]; then + PID="$(cat "$PID_FILE" 2>/dev/null || true)" + if [ -n "${PID:-}" ] && kill -0 "$PID" 2>/dev/null; then + info "检测到 PID=${PID}(来自 $PID_FILE),尝试停止..." + kill "$PID" 2>/dev/null || true + sleep 1 + if kill -0 "$PID" 2>/dev/null; then + warn "进程仍在运行,强制 kill -9 ${PID}" + kill -9 "$PID" 2>/dev/null || true + fi fi - ok "已停止 clash 进程(PIDFile)" + rm -f "$PID_FILE" || true fi -fi +done -# 再兜底:按进程名(系统可能有多个 clash,不建议无脑 pkill -9;先提示再杀) -if pgrep -x clash >/dev/null 2>&1; then - warn "检测到仍有 clash 进程存在(可能非本项目),尝试温和结束..." - pkill -x clash >/dev/null 2>&1 || true - sleep 1 -fi -if pgrep -x clash >/dev/null 2>&1; then - warn "仍残留 clash 进程,执行 pkill -9(可能影响其它 clash 实例)..." - pkill -9 -x clash >/dev/null 2>&1 || true -fi +# 兜底:按完整路径匹配,避免误杀其他 clash +pkill -f '/clash-for-linux/.*/clash' >/dev/null 2>&1 || true +pkill -f '/clash-for-linux/.*/mihomo' >/dev/null 2>&1 || true +sleep 1 +pkill -9 -f '/clash-for-linux/.*/clash' >/dev/null 2>&1 || true +pkill -9 -f '/clash-for-linux/.*/mihomo' >/dev/null 2>&1 || true -# ========================= -# 3) 删除 systemd unit(对标 install_systemd.sh) -# ========================= -if [ -f "$Unit_Path" ]; then - rm -f "$Unit_Path" - ok "已移除 systemd 单元: ${Unit_Path}" +# 3) remove unit and related files +if [ -f "$UNIT_PATH" ]; then + rm -f "$UNIT_PATH" + ok "已移除 systemd 单元: ${UNIT_PATH}" fi - -# drop-in(万一用户自定义过) -if [ -d "/etc/systemd/system/${Service_Name}.service.d" ]; then - rm -rf "/etc/systemd/system/${Service_Name}.service.d" - ok "已移除 drop-in: /etc/systemd/system/${Service_Name}.service.d" +if [ -d "/etc/systemd/system/${SERVICE_NAME}.service.d" ]; then + rm -rf "/etc/systemd/system/${SERVICE_NAME}.service.d" + ok "已移除 drop-in: /etc/systemd/system/${SERVICE_NAME}.service.d" fi - if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload >/dev/null 2>&1 || true systemctl reset-failed >/dev/null 2>&1 || true fi -# ========================= -# 4) 清理默认配置/环境脚本/命令入口 -# ========================= -if [ -f "/etc/default/${Service_Name}" ]; then - rm -f "/etc/default/${Service_Name}" - ok "已移除: /etc/default/${Service_Name}" +# 4) cleanup env / command entry +rm -f "/etc/default/${SERVICE_NAME}" >/dev/null 2>&1 || true +rm -f "/etc/profile.d/clash-for-linux.sh" >/dev/null 2>&1 || true +rm -f "/usr/local/bin/clashctl" >/dev/null 2>&1 || true +for d in "/root/clash-for-linux" "/opt/clash-for-linux" "${INSTALL_DIR:-}"; do + [ -n "$d" ] || continue + rm -f "$d/temp/clash-for-linux.sh" >/dev/null 2>&1 || true +done + +# 5) remove install dirs +removed_any=false +for d in "${INSTALL_DIR:-}" "/root/clash-for-linux" "/opt/clash-for-linux"; do + [ -n "$d" ] || continue + if [ -d "$d" ] && { [ -f "$d/start.sh" ] || [ -d "$d/conf" ] || [ "$d" = "$INSTALL_DIR" ]; }; then + rm -rf "$d" + ok "已移除安装目录: $d" + removed_any=true + fi +done + +if [ "$removed_any" = false ]; then + warn "未发现可删除的安装目录" fi -# 运行时 Env_File 可能写到 /etc/profile.d 或 temp,这里都清 -if [ -f "/etc/profile.d/clash-for-linux.sh" ]; then - rm -f "/etc/profile.d/clash-for-linux.sh" - ok "已移除: /etc/profile.d/clash-for-linux.sh" -fi - -if [ -f "${Install_Dir}/temp/clash-for-linux.sh" ]; then - rm -f "${Install_Dir}/temp/clash-for-linux.sh" || true - ok "已移除: ${Install_Dir}/temp/clash-for-linux.sh" -fi - -if [ -f "/usr/local/bin/clashctl" ]; then - rm -f "/usr/local/bin/clashctl" - ok "已移除: /usr/local/bin/clashctl" -fi - -# ========================= -# 5) 删除安装目录 -# ========================= -if [ -d "$Install_Dir" ]; then - rm -rf "$Install_Dir" - ok "已移除安装目录: ${Install_Dir}" -else - warn "未找到安装目录: ${Install_Dir}" -fi - -# ========================= -# 7) 提示:当前终端代理变量需要手动清 -# ========================= echo warn "如果你曾执行 proxy_on,当前终端可能仍保留代理环境变量。可执行:" echo " unset http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY" echo " # 或关闭终端重新打开" echo -ok "卸载完成(root-only 模式)✅" +ok "卸载完成 ✅"