This commit is contained in:
Arvin
2026-03-21 13:58:10 +08:00
parent f62ea80d43
commit dda67b180d
72 changed files with 848 additions and 1181 deletions

View File

@ -1,16 +1,36 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
trim_value() {
local value="$1"
echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}
# =========================
# 安全写入:避免重复块
# =========================
remove_block_if_exists() {
local file="$1"
local marker="$2"
[ -f "$file" ] || return 0
# 删除已有 block从 marker 到文件结束)
if grep -q "$marker" "$file"; then
sed -i "/$marker/,\$d" "$file"
fi
}
# =========================
# TUN 配置
# =========================
apply_tun_config() {
local config_path="$1"
local enable="${CLASH_TUN_ENABLE:-false}"
if [ "$enable" != "true" ]; then
return 0
fi
[ "$enable" = "true" ] || return 0
remove_block_if_exists "$config_path" "# ==== TUN CONFIG START ===="
local stack="${CLASH_TUN_STACK:-system}"
local auto_route="${CLASH_TUN_AUTO_ROUTE:-true}"
@ -22,65 +42,91 @@ apply_tun_config() {
{
echo ""
echo "# ==== TUN CONFIG START ===="
echo "tun:"
echo " enable: true"
echo " stack: ${stack}"
echo " auto-route: ${auto_route}"
echo " auto-redirect: ${auto_redirect}"
echo " strict-route: ${strict_route}"
if [ -n "$device" ]; then
echo " device: ${device}"
fi
if [ -n "$mtu" ]; then
echo " mtu: ${mtu}"
fi
[ -n "$device" ] && echo " device: ${device}"
[ -n "$mtu" ] && echo " mtu: ${mtu}"
if [ -n "$dns_hijack" ]; then
echo " dns-hijack:"
IFS=',' read -r -a hijacks <<< "$dns_hijack"
for item in "${hijacks[@]}"; do
local trimmed
trimmed=$(trim_value "$item")
if [ -n "$trimmed" ]; then
echo " - ${trimmed}"
fi
item="$(trim_value "$item")"
[ -n "$item" ] && echo " - ${item}"
done
fi
echo "# ==== TUN CONFIG END ===="
} >> "$config_path"
}
# =========================
# MIXIN 配置
# =========================
apply_mixin_config() {
local config_path="$1"
local base_dir="${2:-$Server_Dir}"
local mixin_dir="${CLASH_MIXIN_DIR:-$base_dir/conf/mixin.d}"
local base_dir="$2"
local mixin_dir="${CLASH_MIXIN_DIR:-$base_dir/config/mixin.d}"
local mixin_paths=()
remove_block_if_exists "$config_path" "# ==== MIXIN CONFIG START ===="
# 用户手动指定优先
if [ -n "${CLASH_MIXIN_PATHS:-}" ]; then
IFS=',' read -r -a mixin_paths <<< "$CLASH_MIXIN_PATHS"
fi
# 自动扫描目录(补充)
if [ -d "$mixin_dir" ]; then
while IFS= read -r -d '' file; do
mixin_paths+=("$file")
done < <(find "$mixin_dir" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print0 | sort -z)
done < <(
find "$mixin_dir" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) \
-print0 | sort -z
)
fi
# 去重
local uniq_paths=()
local seen=""
for path in "${mixin_paths[@]}"; do
local trimmed
trimmed=$(trim_value "$path")
if [ -z "$trimmed" ]; then
continue
path="$(trim_value "$path")"
[ -z "$path" ] && continue
# 相对路径转绝对
if [ "${path:0:1}" != "/" ]; then
path="$base_dir/$path"
fi
if [ "${trimmed:0:1}" != "/" ]; then
trimmed="$base_dir/$trimmed"
fi
if [ -f "$trimmed" ]; then
{
echo ""
echo "# ---- mixin: ${trimmed} ----"
cat "$trimmed"
} >> "$config_path"
else
echo "[WARN] Mixin file not found: $trimmed" >&2
if [[ "$seen" != *"|$path|"* ]]; then
uniq_paths+=("$path")
seen="${seen}|$path|"
fi
done
}
# 写入
{
echo ""
echo "# ==== MIXIN CONFIG START ===="
for path in "${uniq_paths[@]}"; do
if [ -f "$path" ]; then
echo ""
echo "# ---- mixin: ${path} ----"
cat "$path"
else
echo "[WARN] Mixin not found: $path" >&2
fi
done
echo "# ==== MIXIN CONFIG END ===="
} >> "$config_path"
}

View File

@ -1,21 +1,6 @@
#!/usr/bin/env bash
set -e
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
RUNTIME_DIR="$PROJECT_DIR/runtime"
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
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
exec "$PROJECT_DIR/clashctl" doctor "$@"

View File

@ -12,6 +12,7 @@ STATE_FILE="$RUNTIME_DIR/state.env"
TMP_DOWNLOAD="$RUNTIME_DIR/subscription.raw.yaml"
TMP_NORMALIZED="$RUNTIME_DIR/subscription.normalized.yaml"
TMP_PROXY_FRAGMENT="$RUNTIME_DIR/proxy.fragment.yaml"
TMP_CONFIG="$RUNTIME_DIR/config.yaml.tmp"
mkdir -p "$RUNTIME_DIR" "$CONFIG_DIR" "$LOG_DIR"
@ -64,6 +65,15 @@ generate_secret() {
return 0
fi
if [ -s "$RUNTIME_CONFIG" ]; then
local old_secret
old_secret="$(sed -nE 's/^[[:space:]]*secret:[[:space:]]*"?([^"#]+)"?.*$/\1/p' "$RUNTIME_CONFIG" | head -n 1)"
if [ -n "${old_secret:-}" ]; then
echo "$old_secret"
return 0
fi
fi
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 16
else
@ -103,9 +113,11 @@ apply_controller_to_config() {
rm -rf "$ui_dir"
mkdir -p "$ui_dir"
cp -a "$PROJECT_DIR/dashboard/public/." "$ui_dir/"
upsert_yaml_kv_local "$file" "external-ui" "$ui_dir"
if [ -d "$PROJECT_DIR/dashboard/public" ]; then
cp -a "$PROJECT_DIR/dashboard/public/." "$ui_dir/"
upsert_yaml_kv_local "$file" "external-ui" "$ui_dir"
fi
fi
}
@ -121,11 +133,32 @@ download_subscription() {
is_complete_clash_config() {
local file="$1"
grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$file"
grep -qE '^[[:space:]]*(proxies:|proxy-providers:|mixed-port:|port:)' "$file"
}
cleanup_tmp_files() {
rm -f "$TMP_NORMALIZED" "$TMP_PROXY_FRAGMENT"
rm -f "$TMP_PROXY_FRAGMENT" "$TMP_CONFIG"
}
build_fragment_config() {
local template_file="$1"
local target_file="$2"
sed -n '/^proxies:/,$p' "$TMP_NORMALIZED" > "$TMP_PROXY_FRAGMENT"
cat "$template_file" > "$target_file"
cat "$TMP_PROXY_FRAGMENT" >> "$target_file"
sed -i "s/CLASH_HTTP_PORT_PLACEHOLDER/${CLASH_HTTP_PORT}/g" "$target_file"
sed -i "s/CLASH_SOCKS_PORT_PLACEHOLDER/${CLASH_SOCKS_PORT}/g" "$target_file"
sed -i "s/CLASH_REDIR_PORT_PLACEHOLDER/${CLASH_REDIR_PORT}/g" "$target_file"
sed -i "s/CLASH_LISTEN_IP_PLACEHOLDER/${CLASH_LISTEN_IP}/g" "$target_file"
sed -i "s/CLASH_ALLOW_LAN_PLACEHOLDER/${CLASH_ALLOW_LAN}/g" "$target_file"
}
finalize_config() {
local file="$1"
mv -f "$file" "$RUNTIME_CONFIG"
}
main() {
@ -156,9 +189,10 @@ main() {
cp -f "$TMP_DOWNLOAD" "$TMP_NORMALIZED"
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"
cp -f "$TMP_NORMALIZED" "$TMP_CONFIG"
apply_controller_to_config "$TMP_CONFIG"
apply_secret_to_config "$TMP_CONFIG"
finalize_config "$TMP_CONFIG"
write_state "success" "subscription_full" "subscription_full"
cleanup_tmp_files
exit 0
@ -171,22 +205,14 @@ main() {
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"
build_fragment_config "$template_file" "$TMP_CONFIG"
apply_controller_to_config "$TMP_CONFIG"
apply_secret_to_config "$TMP_CONFIG"
finalize_config "$TMP_CONFIG"
write_state "success" "subscription_fragment_merged" "subscription_fragment"
cleanup_tmp_files
}
trap cleanup_tmp_files EXIT
main "$@"

View File

@ -10,14 +10,14 @@ SERVICE_GROUP="${CLASH_SERVICE_GROUP:-root}"
RUNTIME_DIR="$PROJECT_DIR/runtime"
LOG_DIR="$PROJECT_DIR/logs"
CONF_DIR="$PROJECT_DIR/conf"
CONFIG_DIR="$PROJECT_DIR/config"
if [ "$(id -u)" -ne 0 ]; then
echo "[ERROR] root required to install systemd unit" >&2
exit 1
fi
install -d -m 0755 "$RUNTIME_DIR" "$LOG_DIR" "$CONF_DIR"
install -d -m 0755 "$RUNTIME_DIR" "$LOG_DIR" "$CONFIG_DIR" "$CONFIG_DIR/mixin.d"
cat >"$UNIT_PATH" <<EOF
[Unit]
@ -35,8 +35,11 @@ Group=${SERVICE_GROUP}
WorkingDirectory=${PROJECT_DIR}
Environment=HOME=/root
ExecStart=/bin/bash ${PROJECT_DIR}/scripts/run_clash.sh --foreground
ExecStop=/bin/bash ${PROJECT_DIR}/clashctl --from-systemd stop
ExecStart=${PROJECT_DIR}/clashctl start
ExecStop=${PROJECT_DIR}/clashctl --from-systemd stop
ExecReload=${PROJECT_DIR}/clashctl restart
PIDFile=${PROJECT_DIR}/runtime/clash.pid
Restart=always
RestartSec=5s
@ -62,4 +65,5 @@ echo "[OK] systemd unit installed: ${UNIT_PATH}"
echo "start : systemctl start ${SERVICE_NAME}.service"
echo "stop : systemctl stop ${SERVICE_NAME}.service"
echo "restart : systemctl restart ${SERVICE_NAME}.service"
echo "reload : systemctl reload ${SERVICE_NAME}.service"
echo "status : systemctl status ${SERVICE_NAME}.service -l --no-pager"

View File

@ -1,20 +1,6 @@
#!/usr/bin/env bash
set -e
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
LOG_FILE="$PROJECT_DIR/logs/clash.log"
SERVICE_NAME="clash-for-linux.service"
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
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
exec "$PROJECT_DIR/clashctl" logs "$@"

View File

@ -1,36 +1,49 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
PORT_CHECK_WARNED=${PORT_CHECK_WARNED:-0}
# =========================
# 判断端口是否被占用(更稳)
# =========================
is_port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
ss -lnt 2>/dev/null | awk '{print $4}' | grep -E "[:.]${port}$" >/dev/null 2>&1
return $?
fi
if command -v netstat >/dev/null 2>&1; then
netstat -lnt | awk '{print $4}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
netstat -lnt 2>/dev/null | awk '{print $4}' | grep -E "[:.]${port}$" >/dev/null 2>&1
return $?
fi
if command -v lsof >/dev/null 2>&1; then
lsof -iTCP -sTCP:LISTEN -P -n | awk '{print $9}' | grep -E "(:|\.)${port}$" >/dev/null 2>&1
lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | awk '{print $9}' | grep -E "[:.]${port}$" >/dev/null 2>&1
return $?
fi
if [ "$PORT_CHECK_WARNED" -eq 0 ]; then
echo -e "\033[33m[WARN] 未找到端口检测工具,端口冲突检测可能不准确\033[0m" >&2
echo "[WARN] no port check tool found (ss/netstat/lsof)" >&2
PORT_CHECK_WARNED=1
fi
return 1
}
# =========================
# 找可用端口(优化版)
# =========================
find_available_port() {
local start_port=${1:-20000}
local end_port=${2:-65000}
local start="${1:-20000}"
local end="${2:-65000}"
local port
# 优先随机尝试
if command -v shuf >/dev/null 2>&1; then
for _ in {1..50}; do
port=$(shuf -i "${start_port}-${end_port}" -n 1)
for _ in {1..30}; do
port=$(shuf -i "${start}-${end}" -n 1)
if ! is_port_in_use "$port"; then
echo "$port"
return 0
@ -38,7 +51,8 @@ find_available_port() {
done
fi
for port in $(seq "$start_port" "$end_port"); do
# fallback 顺序扫描(限制范围避免慢)
for port in $(seq "$start" "$((start + 2000))"); do
if ! is_port_in_use "$port"; then
echo "$port"
return 0
@ -48,43 +62,56 @@ find_available_port() {
return 1
}
# =========================
# 解析端口值(核心函数)
# =========================
resolve_port_value() {
local name="$1"
local value="$2"
local resolved
# auto / 空
if [ -z "$value" ] || [ "$value" = "auto" ]; then
resolved=$(find_available_port)
if [ -z "$resolved" ]; then
resolved=$(find_available_port) || {
echo "[ERROR] ${name} failed to allocate port" >&2
return 1
fi
echo -e "\033[33m[WARN] ${name} 端口已自动分配为 ${resolved}\033[0m" >&2
}
echo "[WARN] ${name} auto assigned: ${resolved}" >&2
echo "$resolved"
return 0
fi
if [[ "$value" =~ ^[0-9]+$ ]]; then
if is_port_in_use "$value"; then
resolved=$(find_available_port)
if [ -n "$resolved" ]; then
echo -e "\033[33m[WARN] ${name} 端口 ${value} 已被占用,已自动切换为 ${resolved}\033[0m" >&2
echo "$resolved"
return 0
fi
# 非数字
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
echo "[ERROR] invalid port: $value" >&2
return 1
fi
# 被占用 → 自动替换
if is_port_in_use "$value"; then
resolved=$(find_available_port)
if [ -n "$resolved" ]; then
echo "[WARN] ${name} port ${value} in use, switched to ${resolved}" >&2
echo "$resolved"
return 0
fi
fi
echo "$value"
}
# =========================
# 解析 host:port
# =========================
resolve_host_port() {
local name="$1"
local raw="$2"
local default_host="$3"
local host
local port
if [ "$raw" = "auto" ] || [ -z "$raw" ]; then
if [ -z "$raw" ] || [ "$raw" = "auto" ]; then
host="$default_host"
port="auto"
else
@ -97,6 +124,10 @@ resolve_host_port() {
fi
fi
# host 兜底
[ -z "$host" ] && host="$default_host"
port=$(resolve_port_value "$name" "$port") || return 1
echo "${host}:${port}"
}
}

View File

@ -1,119 +1,214 @@
#!/bin/bash
resolve_clash_arch() {
local raw_arch="$1"
case "$raw_arch" in
x86_64|amd64)
echo "linux-amd64"
;;
aarch64|arm64)
echo "linux-arm64"
;;
armv7*|armv7l)
echo "linux-armv7"
;;
*)
echo "linux-${raw_arch}"
;;
x86_64|amd64) echo "linux-amd64" ;;
aarch64|arm64) echo "linux-arm64" ;;
armv7*|armv7l) echo "linux-armv7" ;;
*) echo "linux-${raw_arch}" ;;
esac
}
get_latest_mihomo_version() {
local url="https://api.github.com/repos/MetaCubeX/mihomo/releases/latest"
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$url" \
| grep '"tag_name"' \
| sed -E 's/.*"([^"]+)".*/\1/' \
| head -n 1
elif command -v wget >/dev/null 2>&1; then
wget -qO- "$url" \
| grep '"tag_name"' \
| sed -E 's/.*"([^"]+)".*/\1/' \
| head -n 1
fi
}
download_clash_bin() {
local server_dir="$1"
local detected_arch="$2"
local resolved_arch
local version
local download_url
local download_target
local archive_file
local tmp_bin
resolved_arch="$(resolve_clash_arch "$detected_arch")"
resolved_arch=$(resolve_clash_arch "$detected_arch")
if [ -z "$resolved_arch" ]; then
echo -e "\033[33m[WARN] 无法识别 CPU 架构,跳过 Clash 内核自动下载\033[0m"
echo "[WARN] 无法识别 CPU 架构" >&2
return 1
fi
if [ "${CLASH_AUTO_DOWNLOAD:-auto}" = "false" ]; then
version="${MIHOMO_VERSION:-}"
if [ -z "$version" ]; then
version="$(get_latest_mihomo_version || true)"
fi
if [ -z "$version" ]; then
echo "[ERROR] 无法获取 Mihomo 版本" >&2
return 1
fi
local _default_url="https://github.com/Dreamacro/clash/releases/latest/download/clash-{arch}.gz"
download_url="${CLASH_DOWNLOAD_URL_TEMPLATE:-$_default_url}"
if [ -z "$download_url" ]; then
echo -e "\033[33m[WARN] 未设置 CLASH_DOWNLOAD_URL_TEMPLATE跳过 Clash 内核自动下载\033[0m"
if [ -z "${CLASH_DOWNLOAD_URL_TEMPLATE:-}" ]; then
echo "[ERROR] CLASH_DOWNLOAD_URL_TEMPLATE 未设置" >&2
return 1
fi
download_url="${download_url//\{arch\}/${resolved_arch}}"
download_url="${CLASH_DOWNLOAD_URL_TEMPLATE//\{arch\}/${resolved_arch}}"
download_url="${download_url//\{version\}/${version}}"
download_target="${server_dir}/bin/clash-${resolved_arch}"
archive_file="${server_dir}/temp/clash-${resolved_arch}.download"
archive_file="${server_dir}/runtime/.clash_download.tmp"
tmp_bin="${server_dir}/runtime/.clash_bin.tmp"
mkdir -p "${server_dir}/bin" "${server_dir}/temp"
mkdir -p "${server_dir}/bin" "${server_dir}/runtime"
rm -f "$archive_file" "$tmp_bin"
echo "[INFO] downloading: $download_url"
# =========================
# 下载
# =========================
if command -v curl >/dev/null 2>&1; then
curl -L -sS -o "${archive_file}" "${download_url}"
if ! curl -fL -sS -o "$archive_file" "$download_url"; then
echo "[ERROR] 下载失败: $download_url" >&2
return 1
fi
elif command -v wget >/dev/null 2>&1; then
wget -q -O "${archive_file}" "${download_url}"
if ! wget -q -O "$archive_file" "$download_url"; then
echo "[ERROR] 下载失败: $download_url" >&2
return 1
fi
else
echo -e "\033[33m[WARN] 未找到 curl 或 wget无法自动下载 Clash 内核\033[0m"
echo "[ERROR] 未找到 curl 或 wget" >&2
return 1
fi
if [ -f "${archive_file}" ]; then
if gzip -t "${archive_file}" >/dev/null 2>&1; then
gzip -dc "${archive_file}" >"${download_target}"
else
mv "${archive_file}" "${download_target}"
fi
chmod +x "${download_target}"
echo "${download_target}"
return 0
# =========================
# 基础校验(防 404 / HTML
# =========================
if [ ! -s "$archive_file" ]; then
echo "[ERROR] 下载文件为空" >&2
return 1
fi
echo -e "\033[33m[WARN] Clash 内核自动下载失败\033[0m"
return 1
if head -c 200 "$archive_file" | grep -qiE "not found|html"; then
echo "[ERROR] 下载内容疑似错误页面404/HTML" >&2
return 1
fi
# =========================
# 解压 / 直写
# =========================
if gzip -t "$archive_file" >/dev/null 2>&1; then
if ! gzip -dc "$archive_file" > "$tmp_bin"; then
echo "[ERROR] gzip 解压失败" >&2
return 1
fi
else
cp "$archive_file" "$tmp_bin"
fi
# =========================
# ELF 校验(关键)
# =========================
if ! file "$tmp_bin" | grep -q "ELF"; then
echo "[ERROR] 非有效 ELF 二进制" >&2
echo "[DEBUG] file result: $(file "$tmp_bin")" >&2
return 1
fi
chmod +x "$tmp_bin"
mv "$tmp_bin" "$download_target"
rm -f "$archive_file"
echo "[OK] downloaded: $download_target"
echo "$download_target"
}
resolve_clash_bin() {
local server_dir="$1"
local detected_arch="$2"
local resolved_arch
local candidates=()
local candidate
local downloaded_bin
local mode
if [ -n "${CLASH_BIN:-}" ]; then
if [ -x "$CLASH_BIN" ]; then
echo "$CLASH_BIN"
return 0
fi
echo -e "\033[31m[ERROR] CLASH_BIN 指定的文件不可执行: $CLASH_BIN\033[0m"
echo "[ERROR] CLASH_BIN 不可执行: $CLASH_BIN" >&2
return 1
fi
resolved_arch=$(resolve_clash_arch "$detected_arch")
resolved_arch="$(resolve_clash_arch "$detected_arch")"
if [ -n "$resolved_arch" ]; then
candidates+=("${server_dir}/bin/clash-${resolved_arch}")
fi
candidates+=(
"${server_dir}/bin/clash-${detected_arch}"
"${server_dir}/bin/clash"
)
for candidate in "${candidates[@]}"; do
if [ -x "$candidate" ]; then
echo "$candidate"
return 0
fi
done
mode="${CLASH_AUTO_DOWNLOAD:-auto}"
if downloaded_bin=$(download_clash_bin "$server_dir" "$detected_arch"); then
echo "$downloaded_bin"
return 0
fi
case "$mode" in
false)
for candidate in "${candidates[@]}"; do
if [ -x "$candidate" ]; then
echo "$candidate"
return 0
fi
done
;;
echo -e "\033[31m\n[ERROR] 未找到可用的 Clash 二进制。\033[0m"
echo -e "请将对应架构的二进制放入: $server_dir/bin/"
echo -e "可用命名示例: clash-${resolved_arch} 或 clash-${detected_arch}"
echo -e "或通过 CLASH_BIN 指定自定义路径。"
auto)
for candidate in "${candidates[@]}"; do
if [ -x "$candidate" ]; then
echo "$candidate"
return 0
fi
done
if downloaded_bin="$(download_clash_bin "$server_dir" "$detected_arch")"; then
echo "$downloaded_bin"
return 0
fi
;;
true)
if downloaded_bin="$(download_clash_bin "$server_dir" "$detected_arch")"; then
echo "$downloaded_bin"
return 0
fi
for candidate in "${candidates[@]}"; do
if [ -x "$candidate" ]; then
echo "$candidate"
return 0
fi
done
;;
*)
echo "[ERROR] CLASH_AUTO_DOWNLOAD 非法值: $mode" >&2
return 1
;;
esac
echo "[ERROR] 未找到可用 Mihomo 内核" >&2
echo "请放入: ${server_dir}/bin/" >&2
return 1
}
}

View File

@ -12,7 +12,9 @@ mkdir -p "$RUNTIME_DIR" "$LOG_DIR"
FOREGROUND=false
DAEMON=false
# 解析参数
# =========================
# 参数解析
# =========================
for arg in "$@"; do
case "$arg" in
--foreground) FOREGROUND=true ;;
@ -29,6 +31,14 @@ if [ "$FOREGROUND" = true ] && [ "$DAEMON" = true ]; then
exit 2
fi
if [ "$FOREGROUND" = false ] && [ "$DAEMON" = false ]; then
echo "[ERROR] Must specify --foreground or --daemon" >&2
exit 2
fi
# =========================
# 基础校验
# =========================
if [ ! -s "$CONFIG_FILE" ]; then
echo "[ERROR] runtime config not found: $CONFIG_FILE" >&2
exit 2
@ -39,6 +49,9 @@ if grep -q '\${' "$CONFIG_FILE"; then
exit 2
fi
# =========================
# 加载依赖
# =========================
# shellcheck disable=SC1091
source "$PROJECT_DIR/scripts/get_cpu_arch.sh"
# shellcheck disable=SC1091
@ -46,40 +59,49 @@ 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
echo "[ERROR] clash binary not executable: $CLASH_BIN" >&2
exit 2
fi
test_config() {
local bin="$1"
local config="$2"
local runtime_dir="$3"
"$bin" -d "$runtime_dir" -t -f "$config" >/dev/null 2>&1
}
if ! test_config "$CLASH_BIN" "$CONFIG_FILE" "$RUNTIME_DIR"; then
echo "[ERROR] config test failed: $CONFIG_FILE" >&2
# =========================
# config 测试(唯一一次)
# =========================
if ! "$CLASH_BIN" -t -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >/dev/null 2>&1; then
echo "[ERROR] clash config test failed: $CONFIG_FILE" >&2
write_run_state "failed" "config-test"
exit 2
fi
# systemd 模式
# =========================
# 前台模式systemd
# =========================
if [ "$FOREGROUND" = true ]; then
write_run_state "running" "systemd"
exec "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR"
fi
# script / daemon 模式
if [ "$DAEMON" = true ]; then
nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 &
pid=$!
echo "$pid" > "$PID_FILE"
write_run_state "running" "script" "$pid"
echo "[OK] Clash started in script mode, pid=$pid"
# =========================
# 后台模式script
# =========================
cleanup_dead_pid
if is_script_running; then
pid="$(read_pid 2>/dev/null || true)"
echo "[INFO] clash already running, pid=${pid:-unknown}"
exit 0
fi
echo "[ERROR] Must specify --foreground or --daemon" >&2
exit 2
nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 &
pid=$!
echo "$pid" > "$PID_FILE"
write_run_state "running" "script" "$pid"
echo "[OK] Clash started in script mode, pid=$pid"

View File

@ -9,6 +9,9 @@ SERVICE_NAME="clash-for-linux.service"
mkdir -p "$RUNTIME_DIR"
# =========================
# 基础能力
# =========================
has_systemd() {
command -v systemctl >/dev/null 2>&1
}
@ -19,17 +22,39 @@ service_unit_exists() {
}
read_pid() {
[ -f "$PID_FILE" ] || return 1
cat "$PID_FILE"
[ -s "$PID_FILE" ] || return 1
tr -d '[:space:]' < "$PID_FILE"
}
is_pid_running() {
local pid="$1"
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
}
is_script_running() {
local pid
pid="$(read_pid 2>/dev/null || true)"
[ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null
is_pid_running "$pid"
}
# =========================
# 清理僵尸 PID关键
# =========================
cleanup_dead_pid() {
local pid
pid="$(read_pid 2>/dev/null || true)"
if [ -n "${pid:-}" ] && ! is_pid_running "$pid"; then
rm -f "$PID_FILE"
fi
}
# =========================
# 模式检测(统一)
# =========================
detect_mode() {
cleanup_dead_pid
if service_unit_exists && systemctl is-active --quiet "$SERVICE_NAME"; then
echo "systemd"
elif is_script_running; then
@ -41,59 +66,65 @@ detect_mode() {
fi
}
write_run_state() {
local status="$1"
local mode="${2:-unknown}"
local pid="${3:-}"
# =========================
# state 写入(唯一实现)
# =========================
write_state_kv() {
local key="$1"
local value="$2"
mkdir -p "$RUNTIME_DIR"
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"
if grep -q "^${key}=" "$STATE_FILE" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${value}|" "$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
echo "${key}=${value}" >> "$STATE_FILE"
fi
}
write_run_state() {
local status="${1:-unknown}"
local mode="${2:-unknown}"
local pid="${3:-}"
write_state_kv "LAST_RUN_STATUS" "$status"
write_state_kv "LAST_RUN_MODE" "$mode"
write_state_kv "LAST_RUN_AT" "$(date -Iseconds)"
if [ -n "$pid" ]; then
write_state_kv "LAST_RUN_PID" "$pid"
fi
}
# =========================
# systemd 模式
# =========================
start_via_systemd() {
systemctl start "$SERVICE_NAME"
}
stop_via_systemd() {
systemctl stop "$SERVICE_NAME"
systemctl stop "$SERVICE_NAME" || true
cleanup_dead_pid
write_run_state "stopped" "systemd"
rm -f "$PID_FILE"
}
restart_via_systemd() {
systemctl restart "$SERVICE_NAME"
}
# =========================
# script 模式
# =========================
start_via_script() {
cleanup_dead_pid
if is_script_running; then
echo "[INFO] clash already running (script mode)"
echo "[INFO] clash already running (script)"
return 0
fi
"$PROJECT_DIR/scripts/run_clash.sh" --daemon
}
@ -101,11 +132,19 @@ stop_via_script() {
local pid
pid="$(read_pid 2>/dev/null || true)"
if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then
if [ -n "${pid:-}" ] && is_pid_running "$pid"; then
echo "[INFO] stopping clash pid=$pid"
kill "$pid"
sleep 1
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
for _ in 1 2 3 4 5; do
if ! is_pid_running "$pid"; then
break
fi
sleep 1
done
if is_pid_running "$pid"; then
kill -9 "$pid" 2>/dev/null || true
fi
fi

View File

@ -1,28 +1,6 @@
#!/usr/bin/env bash
set -e
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[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
exec "$PROJECT_DIR/clashctl" status "$@"

View File

@ -1,23 +1,6 @@
#!/usr/bin/env bash
set -e
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[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
exec "$PROJECT_DIR/clashctl" stop "$@"