clashctl conf\backup.yaml runtime\config.yaml scripts\generate_config.sh scripts\install_systemd.sh scripts\run_clash.sh scripts\service_lib.sh

This commit is contained in:
wnlen
2026-03-20 16:51:29 +08:00
parent df37425226
commit 771ef38039
7 changed files with 924 additions and 214 deletions

738
clashctl
View File

@ -1,12 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Service_Name="clash-for-linux.service" SERVICE_NAME="clash-for-linux.service"
PROFILED_FILE="/etc/profile.d/clash-for-linux.sh" PROFILED_FILE="/etc/profile.d/clash-for-linux.sh"
ENV_FILE="$Server_Dir/.env" ENV_FILE="$PROJECT_DIR/.env"
CONF_FILE="$Server_Dir/conf/config.yaml" RUNTIME_DIR="$PROJECT_DIR/runtime"
TEMP_CONF_FILE="$Server_Dir/temp/config.yaml" RUNTIME_CONFIG="$RUNTIME_DIR/config.yaml"
STATE_FILE="$RUNTIME_DIR/state.env"
# shellcheck disable=SC1091
source "$PROJECT_DIR/scripts/service_lib.sh"
log() { printf "%b\n" "$*"; } log() { printf "%b\n" "$*"; }
info() { log "\033[36m[INFO]\033[0m $*"; } info() { log "\033[36m[INFO]\033[0m $*"; }
@ -19,34 +23,43 @@ usage() {
Usage: Usage:
clashctl COMMAND [OPTIONS] clashctl COMMAND [OPTIONS]
Commands: Core Commands:
on 开启代理 on 开启当前终端代理
off 关闭代理 off 关闭当前终端代理
status 内核状况 start 启动 Clash
proxy 系统代理 stop 停止 Clash
ui Web 面板 restart 重新生成配置并重启
secret Web 密钥 status 查看当前状态
sub 订阅管理 update git pull + 生成配置 + 重启
upgrade 升级内核 generate 仅生成配置,不启动
tun Tun 模式 mode 查看当前运行模式systemd/script/none
mixin Mixin 配置
Global Options: Utility Commands:
ui 输出 Dashboard 地址
secret 输出当前 secret
sub show 查看订阅地址
sub update 重新生成配置并重启
tun status|on|off 查看/启用/关闭 Tun
mixin status|on|off 查看/启用/关闭 Mixin
doctor 健康检查
Options:
--from-systemd 内部使用,避免 stop 递归调用 systemctl
-h, --help 显示帮助信息 -h, --help 显示帮助信息
Examples: Examples:
clashctl on clashctl on
clashctl off clashctl off
clashctl start
clashctl stop
clashctl restart
clashctl status clashctl status
clashctl proxy status clashctl update
clashctl generate
clashctl ui clashctl ui
clashctl secret clashctl secret
clashctl sub show clashctl sub show
clashctl sub update clashctl tun on
clashctl tun status
clashctl mixin status
💡 clashon 同 clashctl onTab 补全更方便!
EOF EOF
} }
@ -57,37 +70,36 @@ require_profiled() {
fi fi
} }
has_systemd() { read_runtime_config_value() {
command -v systemctl >/dev/null 2>&1
}
service_exists() {
has_systemd || return 1
local load_state
load_state="$(systemctl show "$Service_Name" --property=LoadState --value 2>/dev/null || true)"
[ -n "$load_state" ] && [ "$load_state" != "not-found" ]
}
service_is_active() {
service_exists || return 1
systemctl is-active --quiet "$Service_Name"
}
read_config_value() {
local key="$1" local key="$1"
local f [ -f "$RUNTIME_CONFIG" ] || return 1
for f in "$TEMP_CONF_FILE" "$CONF_FILE"; do
if [ -f "$f" ]; then sed -nE "s/^[[:space:]]*${key}:[[:space:]]*//p" "$RUNTIME_CONFIG" \
sed -nE "s/^[[:space:]]*${key}:[[:space:]]*//p" "$f" \
| head -n 1 \ | head -n 1 \
| tr -d '\r' \ | tr -d '\r' \
| sed -E 's/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/' | sed -E 's/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/'
return 0 }
write_env_bool() {
local key="$1"
local value="$2"
if [ ! -f "$ENV_FILE" ]; then
err "未找到 .env: $ENV_FILE"
exit 1
fi fi
done
return 1 if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$ENV_FILE"; then
sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*$|export ${key}=\"${value}\"|g" "$ENV_FILE"
else
echo "export ${key}=\"${value}\"" >> "$ENV_FILE"
fi
}
read_state_value() {
local key="$1"
[ -f "$STATE_FILE" ] || return 1
sed -nE "s/^${key}=(.*)$/\1/p" "$STATE_FILE" | head -n 1
} }
cmd_on() { cmd_on() {
@ -104,63 +116,237 @@ cmd_off() {
proxy_off proxy_off
} }
cmd_status() { cmd_mode() {
local proc_info="" detect_mode
proc_info="$(ps -eo pid,ppid,cmd | grep -E '[c]lash-linux|[m]ihomo' || true)"
echo "=== Clash Status ==="
if service_exists; then
local active_state enabled_state
active_state="$(systemctl is-active "$Service_Name" 2>/dev/null || true)"
enabled_state="$(systemctl is-enabled "$Service_Name" 2>/dev/null || true)"
echo "Service: installed"
echo "Active : ${active_state:-unknown}"
echo "Enabled: ${enabled_state:-unknown}"
if [ -n "$proc_info" ]; then
echo
echo "Process:"
echo "$proc_info"
fi
return 0
fi
if [ -n "$proc_info" ]; then
warn "未检测到 systemd 服务,但发现 Clash 进程正在运行"
echo
echo "Process:"
echo "$proc_info"
return 0
fi
warn "未检测到 systemd 服务,也未发现 Clash 进程"
return 1
} }
cmd_proxy() { cmd_start() {
require_profiled local mode
# shellcheck disable=SC1090 mode="$(detect_mode)"
source "$PROFILED_FILE"
local sub="${1:-status}" case "$mode" in
case "$sub" in systemd)
on) proxy_on ;; start_via_systemd
off) proxy_off ;; ok "Clash started via systemd"
status) proxy_status ;; ;;
script|none)
start_via_script
;;
*) *)
err "未知 proxy 子命令: $sub" err "未知模式: $mode"
echo "用法: clashctl proxy [on|off|status]"
exit 1 exit 1
;; ;;
esac esac
} }
cmd_stop() {
local from_systemd="${1:-false}"
local mode
mode="$(detect_mode)"
case "$mode" in
systemd)
if [ "$from_systemd" = "true" ]; then
# 被 systemd ExecStop 调用时,不能再反向 systemctl stop 自己
ok "Stop requested from systemd, skip recursive systemctl stop"
exit 0
fi
stop_via_systemd
ok "Clash stopped via systemd"
;;
script)
stop_via_script
ok "Clash stopped via script mode"
;;
none)
info "Clash is not running"
;;
*)
err "未知模式: $mode"
exit 1
;;
esac
}
cmd_generate() {
"$PROJECT_DIR/scripts/generate_config.sh"
ok "Config generated"
}
cmd_restart() {
"$PROJECT_DIR/scripts/generate_config.sh"
local mode
mode="$(detect_mode)"
case "$mode" in
systemd)
restart_via_systemd
ok "Clash restarted via systemd"
;;
script|none)
restart_via_script
ok "Clash restarted via script mode"
;;
*)
err "未知模式: $mode"
exit 1
;;
esac
}
cmd_update() {
git -C "$PROJECT_DIR" pull
"$PROJECT_DIR/scripts/generate_config.sh"
local mode
mode="$(detect_mode)"
case "$mode" in
systemd)
restart_via_systemd
ok "Project updated and restarted via systemd"
;;
script|none)
restart_via_script
ok "Project updated and restarted via script mode"
;;
*)
err "未知模式: $mode"
exit 1
;;
esac
}
cmd_status() {
local mode running="no"
mode="$(detect_mode)"
case "$mode" in
systemd)
if systemctl is-active --quiet "$SERVICE_NAME"; then
running="yes"
fi
;;
script)
if is_script_running; then
running="yes"
fi
;;
none)
running="no"
;;
esac
echo "=== Clash Status ==="
echo "Mode : $mode"
echo "Running : $running"
echo "Config : $RUNTIME_CONFIG"
if [ "$mode" = "systemd" ] && service_unit_exists; then
echo "Service : installed"
echo "Active : $(systemctl is-active "$SERVICE_NAME" 2>/dev/null || true)"
echo "Enabled : $(systemctl is-enabled "$SERVICE_NAME" 2>/dev/null || true)"
fi
if [ "$mode" = "script" ]; then
local pid
pid="$(read_pid 2>/dev/null || true)"
echo "PID : ${pid:-unknown}"
fi
if [ -f "$STATE_FILE" ]; then
echo "LastStatus : $(read_state_value LAST_GENERATE_STATUS || true)"
echo "LastReason : $(read_state_value LAST_GENERATE_REASON || true)"
echo "LastSource : $(read_state_value LAST_CONFIG_SOURCE || true)"
echo "LastAt : $(read_state_value LAST_GENERATE_AT || true)"
fi
local controller
controller="$(read_runtime_config_value "external-controller" || true)"
if [ -n "${controller:-}" ]; then
echo "Dashboard : $(cmd_ui --raw)"
fi
}
doctor_ok() {
printf "\033[32m[OK]\033[0m %s\n" "$*"
}
doctor_warn() {
printf "\033[33m[WARN]\033[0m %s\n" "$*"
}
doctor_err() {
printf "\033[31m[ERROR]\033[0m %s\n" "$*"
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
port_from_controller() {
local controller
controller="$(read_runtime_config_value "external-controller" || true)"
if [ -n "${controller:-}" ]; then
printf '%s\n' "${controller##*:}"
else
printf '9090\n'
fi
}
http_port_from_config() {
local v
v="$(read_runtime_config_value "mixed-port" || true)"
if [ -n "${v:-}" ]; then
printf '%s\n' "$v"
return 0
fi
v="$(read_runtime_config_value "port" || true)"
if [ -n "${v:-}" ]; then
printf '%s\n' "$v"
return 0
fi
printf '7890\n'
}
check_port_listening() {
local port="$1"
if command_exists ss; then
ss -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$"
return $?
elif command_exists netstat; then
netstat -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${port}$"
return $?
fi
return 2
}
check_dashboard_http() {
local url="$1"
if command_exists curl; then
curl -fsS --max-time 3 "$url" >/dev/null 2>&1
return $?
elif command_exists wget; then
wget -q -T 3 -O /dev/null "$url" >/dev/null 2>&1
return $?
fi
return 2
}
cmd_ui() { cmd_ui() {
local raw="${1:-}"
local controller host port local controller host port
controller="$(read_config_value "external-controller" || true)"
controller="$(read_runtime_config_value "external-controller" || true)"
[ -n "${controller:-}" ] || controller="127.0.0.1:9090" [ -n "${controller:-}" ] || controller="127.0.0.1:9090"
host="${controller%:*}" host="${controller%:*}"
@ -169,16 +355,22 @@ cmd_ui() {
case "$host" in case "$host" in
0.0.0.0|::|localhost) 0.0.0.0|::|localhost)
host="$(hostname -I 2>/dev/null | awk '{print $1}')" host="$(hostname -I 2>/dev/null | awk '{print $1}')"
[ -n "$host" ] || host="127.0.0.1" [ -n "${host:-}" ] || host="127.0.0.1"
;; ;;
esac esac
if [ "$raw" = "--raw" ]; then
printf 'http://%s:%s/ui\n' "$host" "$port"
return 0
fi
echo "Dashboard URL:"
printf 'http://%s:%s/ui\n' "$host" "$port" printf 'http://%s:%s/ui\n' "$host" "$port"
} }
cmd_secret() { cmd_secret() {
local secret local secret
secret="$(read_config_value "secret" || true)" secret="$(read_runtime_config_value "secret" || true)"
if [ -n "${secret:-}" ]; then if [ -n "${secret:-}" ]; then
echo "$secret" echo "$secret"
else else
@ -189,27 +381,18 @@ cmd_secret() {
cmd_sub() { cmd_sub() {
local subcmd="${1:-show}" local subcmd="${1:-show}"
case "$subcmd" in case "$subcmd" in
show) show)
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_URL=' "$ENV_FILE" || true grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_URL=' "$ENV_FILE" || echo "CLASH_URL=未配置"
else else
err "未找到 .env" err "未找到 .env"
exit 1 exit 1
fi fi
;; ;;
update) update)
if service_exists; then cmd_restart
info "重启服务以刷新订阅..."
systemctl restart "$Service_Name"
ok "订阅刷新完成"
elif [ -f "$Server_Dir/restart.sh" ]; then
bash "$Server_Dir/restart.sh"
ok "订阅刷新完成"
else
err "未找到 restart.sh"
exit 1
fi
;; ;;
*) *)
err "未知 sub 子命令: $subcmd" err "未知 sub 子命令: $subcmd"
@ -219,33 +402,9 @@ cmd_sub() {
esac esac
} }
cmd_upgrade() {
if [ -f "$Server_Dir/update.sh" ]; then
bash "$Server_Dir/update.sh"
else
err "未找到 update.sh"
exit 1
fi
}
write_env_bool() {
local key="$1"
local value="$2"
if [ ! -f "$ENV_FILE" ]; then
err "未找到 .env"
exit 1
fi
if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$ENV_FILE"; then
sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*$|export ${key}=\"${value}\"|g" "$ENV_FILE"
else
echo "export ${key}=\"${value}\"" >> "$ENV_FILE"
fi
}
cmd_tun() { cmd_tun() {
local subcmd="${1:-status}" local subcmd="${1:-status}"
case "$subcmd" in case "$subcmd" in
status) status)
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
@ -265,7 +424,7 @@ cmd_tun() {
;; ;;
*) *)
err "未知 tun 子命令: $subcmd" err "未知 tun 子命令: $subcmd"
echo "用法: clashctl tun [on|off|status]" echo "用法: clashctl tun [status|on|off]"
exit 1 exit 1
;; ;;
esac esac
@ -273,46 +432,307 @@ cmd_tun() {
cmd_mixin() { cmd_mixin() {
local subcmd="${1:-status}" local subcmd="${1:-status}"
local mixin_file="$Server_Dir/conf/mixin.yaml"
case "$subcmd" in case "$subcmd" in
status) status)
if [ -f "$mixin_file" ]; then if [ -f "$ENV_FILE" ]; then
ok "Mixin 已存在: $mixin_file" grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_MIXIN=' "$ENV_FILE" || echo 'CLASH_MIXIN=未配置'
else else
warn "Mixin 不存在: $mixin_file" err "未找到 .env"
exit 1
fi fi
;; ;;
edit) on)
${EDITOR:-vi} "$mixin_file" write_env_bool "CLASH_MIXIN" "true"
ok "已写入 CLASH_MIXIN=true"
;;
off)
write_env_bool "CLASH_MIXIN" "false"
ok "已写入 CLASH_MIXIN=false"
;; ;;
*) *)
err "未知 mixin 子命令: $subcmd" err "未知 mixin 子命令: $subcmd"
echo "用法: clashctl mixin [status|edit]" echo "用法: clashctl mixin [status|on|off]"
exit 1 exit 1
;; ;;
esac esac
} }
main() { cmd_doctor() {
local cmd="${1:-}" local mode running="no"
shift || true local failed=0
local warned=0
case "$cmd" in local controller dashboard_url dashboard_port
on) cmd_on "$@" ;; local http_port
off) cmd_off "$@" ;; local secret
status) cmd_status "$@" ;; local last_generate_status last_generate_reason last_config_source
proxy) cmd_proxy "$@" ;;
ui) cmd_ui "$@" ;; mode="$(detect_mode)"
secret) cmd_secret "$@" ;;
sub) cmd_sub "$@" ;; echo "=== Clash Doctor ==="
upgrade) cmd_upgrade "$@" ;; echo "Project : $PROJECT_DIR"
tun) cmd_tun "$@" ;; echo "Mode : $mode"
mixin) cmd_mixin "$@" ;; echo "Config : $RUNTIME_CONFIG"
-h|--help|"") usage ;;
*)
err "未知命令: $cmd"
echo echo
# 1. 检查运行配置是否存在
if [ -s "$RUNTIME_CONFIG" ]; then
doctor_ok "runtime config exists: $RUNTIME_CONFIG"
else
doctor_err "runtime config missing or empty: $RUNTIME_CONFIG"
failed=1
fi
# 2. 检查配置里是否还有未渲染占位符
if [ -f "$RUNTIME_CONFIG" ]; then
if grep -q '\${' "$RUNTIME_CONFIG"; then
doctor_err "runtime config contains unresolved placeholders"
failed=1
else
doctor_ok "runtime config has no unresolved placeholders"
fi
fi
# 3. 检查 state.env
if [ -f "$STATE_FILE" ]; then
doctor_ok "state file exists: $STATE_FILE"
last_generate_status="$(read_state_value LAST_GENERATE_STATUS || true)"
last_generate_reason="$(read_state_value LAST_GENERATE_REASON || true)"
last_config_source="$(read_state_value LAST_CONFIG_SOURCE || true)"
if [ -n "${last_generate_status:-}" ]; then
if [ "$last_generate_status" = "success" ]; then
doctor_ok "last generate status: success (${last_generate_reason:-unknown})"
else
doctor_warn "last generate status: ${last_generate_status:-unknown} (${last_generate_reason:-unknown})"
warned=1
fi
else
doctor_warn "state file exists but LAST_GENERATE_STATUS is empty"
warned=1
fi
if [ -n "${last_config_source:-}" ]; then
doctor_ok "config source: $last_config_source"
fi
else
doctor_warn "state file missing: $STATE_FILE"
warned=1
fi
# 4. 检查运行模式 / 进程状态
case "$mode" in
systemd)
if systemctl is-active --quiet "$SERVICE_NAME"; then
running="yes"
doctor_ok "systemd service is active"
else
doctor_err "systemd service is installed but not active"
failed=1
fi
;;
script)
if is_script_running; then
running="yes"
doctor_ok "script mode process is running (pid=$(read_pid 2>/dev/null || echo unknown))"
else
doctor_err "script mode detected but PID is not running"
failed=1
fi
;;
systemd-installed)
doctor_warn "systemd unit exists but service is not running"
warned=1
;;
none)
doctor_warn "no running instance detected"
warned=1
;;
*)
doctor_err "unknown mode: $mode"
failed=1
;;
esac
# 5. 检查 dashboard 地址
dashboard_url="$(cmd_ui --raw 2>/dev/null || true)"
controller="$(read_runtime_config_value "external-controller" || true)"
dashboard_port="$(port_from_controller)"
if [ -n "${controller:-}" ]; then
doctor_ok "external-controller configured: $controller"
else
doctor_warn "external-controller not found in runtime config, fallback assumed: 127.0.0.1:9090"
warned=1
fi
if [ -n "${dashboard_url:-}" ]; then
doctor_ok "dashboard url: $dashboard_url"
else
doctor_warn "dashboard url could not be derived"
warned=1
fi
# 6. 检查 HTTP 代理端口
http_port="$(http_port_from_config)"
if check_port_listening "$http_port"; then
doctor_ok "proxy port is listening: $http_port"
else
rc=$?
if [ "$rc" -eq 2 ]; then
doctor_warn "cannot verify proxy port (ss/netstat not available)"
warned=1
else
if [ "$running" = "yes" ]; then
doctor_err "proxy port is not listening: $http_port"
failed=1
else
doctor_warn "proxy port is not listening: $http_port"
warned=1
fi
fi
fi
# 7. 检查 dashboard 端口
if check_port_listening "$dashboard_port"; then
doctor_ok "dashboard port is listening: $dashboard_port"
else
rc=$?
if [ "$rc" -eq 2 ]; then
doctor_warn "cannot verify dashboard port (ss/netstat not available)"
warned=1
else
if [ "$running" = "yes" ]; then
doctor_err "dashboard port is not listening: $dashboard_port"
failed=1
else
doctor_warn "dashboard port is not listening: $dashboard_port"
warned=1
fi
fi
fi
# 8. 检查 dashboard HTTP 可访问性
if [ -n "${dashboard_url:-}" ]; then
if check_dashboard_http "$dashboard_url"; then
doctor_ok "dashboard http reachable"
else
rc=$?
if [ "$rc" -eq 2 ]; then
doctor_warn "cannot verify dashboard http (curl/wget not available)"
warned=1
else
doctor_warn "dashboard http not reachable: $dashboard_url"
warned=1
fi
fi
fi
# 9. 检查 secret
secret="$(read_runtime_config_value "secret" || true)"
if [ -n "${secret:-}" ]; then
doctor_ok "secret exists in runtime config"
else
doctor_warn "secret missing in runtime config"
warned=1
fi
# 10. 检查 clashctl 安装位置(可选)
if command_exists clashctl; then
doctor_ok "clashctl command available: $(command -v clashctl)"
else
doctor_warn "clashctl command not found in PATH"
warned=1
fi
# 11. 检查代理快捷函数文件
if [ -f "$PROFILED_FILE" ]; then
doctor_ok "shell proxy helper exists: $PROFILED_FILE"
else
doctor_warn "shell proxy helper missing: $PROFILED_FILE"
warned=1
fi
echo
if [ "$failed" -ne 0 ]; then
doctor_err "doctor result: FAILED"
return 1
fi
if [ "$warned" -ne 0 ]; then
doctor_warn "doctor result: WARN"
return 0
fi
doctor_ok "doctor result: HEALTHY"
return 0
}
main() {
local from_systemd="false"
if [ "${1:-}" = "--from-systemd" ]; then
from_systemd="true"
shift
fi
case "${1:-}" in
on)
cmd_on
;;
off)
cmd_off
;;
start)
cmd_start
;;
stop)
cmd_stop "$from_systemd"
;;
restart)
cmd_restart
;;
status)
cmd_status
;;
update)
cmd_update
;;
generate)
cmd_generate
;;
mode)
cmd_mode
;;
ui)
shift || true
cmd_ui "${1:-}"
;;
secret)
cmd_secret
;;
sub)
shift || true
cmd_sub "${1:-show}"
;;
tun)
shift || true
cmd_tun "${1:-status}"
;;
mixin)
shift || true
cmd_mixin "${1:-status}"
;;
doctor)
cmd_doctor
;;
""|-h|--help)
usage
;;
*)
err "未知命令: ${1:-}"
usage usage
exit 1 exit 1
;; ;;

0
conf/backup.yaml Normal file
View File

0
runtime/config.yaml Normal file
View File

174
scripts/generate_config.sh Normal file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env bash
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"
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"
# shellcheck disable=SC1091
source "$PROJECT_DIR/.env"
# 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/config_utils.sh"
# shellcheck disable=SC1091
source "$PROJECT_DIR/scripts/port_utils.sh"
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:-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}"
ALLOW_INSECURE_TLS="${ALLOW_INSECURE_TLS:-false}"
CLASH_AUTO_UPDATE="${CLASH_AUTO_UPDATE:-true}"
CLASH_URL="${CLASH_URL:-}"
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")"
write_state() {
local status="$1"
local reason="$2"
local source="${3:-unknown}"
cat > "$STATE_FILE" <<EOF
LAST_GENERATE_STATUS=$status
LAST_GENERATE_REASON=$reason
LAST_CONFIG_SOURCE=$source
LAST_GENERATE_AT=$(date -Iseconds)
EOF
}
generate_secret() {
if [ -n "${CLASH_SECRET:-}" ]; then
echo "$CLASH_SECRET"
return
fi
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 16
else
head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n'
fi
}
SECRET="$(generate_secret)"
upsert_yaml_kv() {
local file="$1" key="$2" value="$3"
[ -f "$file" ] || touch "$file"
if grep -qE "^[[:space:]]*${key}:" "$file"; then
sed -i -E "s|^[[:space:]]*${key}:.*$|${key}: ${value}|g" "$file"
else
printf "%s: %s\n" "$key" "$value" >> "$file"
fi
}
force_write_secret() {
local file="$1"
upsert_yaml_kv "$file" "secret" "$SECRET"
}
force_write_controller_and_ui() {
local file="$1"
if [ "$EXTERNAL_CONTROLLER_ENABLED" = "true" ]; then
upsert_yaml_kv "$file" "external-controller" "$EXTERNAL_CONTROLLER"
mkdir -p "$CONF_DIR"
ln -sfn "$PROJECT_DIR/dashboard/public" "$CONF_DIR/ui"
upsert_yaml_kv "$file" "external-ui" "$CONF_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")
[ "$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() {
local file="$1"
grep -qE '^(proxies:|proxy-providers:|mixed-port:|port:)' "$file"
}
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
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
# 片段订阅:这里先保留模板拼接逻辑
if [ ! -s "$CONF_DIR/template_config.yaml" ]; then
echo "[ERROR] missing template_config.yaml" >&2
write_state "failed" "missing_template" "none"
exit 1
fi
sed -n '/^proxies:/,$p' "$TEMP_CONVERTED" > "$TEMP_DIR/proxy.txt"
cat "$CONF_DIR/template_config.yaml" > "$RUNTIME_CONFIG"
cat "$TEMP_DIR/proxy.txt" >> "$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"
force_write_controller_and_ui "$RUNTIME_CONFIG"
force_write_secret "$RUNTIME_CONFIG"
write_state "success" "subscription_fragment_merged" "subscription_fragment"
}
main "$@"

View File

@ -1,35 +1,25 @@
#!/bin/bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
#################### 基本变量 #################### PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SERVICE_NAME="clash-for-linux"
UNIT_PATH="/etc/systemd/system/${SERVICE_NAME}.service"
Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SERVICE_USER="${CLASH_SERVICE_USER:-root}"
Service_Name="clash-for-linux" SERVICE_GROUP="${CLASH_SERVICE_GROUP:-root}"
Service_User="root" RUNTIME_DIR="$PROJECT_DIR/runtime"
Service_Group="root" LOG_DIR="$PROJECT_DIR/logs"
CONF_DIR="$PROJECT_DIR/conf"
Unit_Path="/etc/systemd/system/${Service_Name}.service"
Env_File="$Server_Dir/temp/clash-for-linux.sh"
#################### 权限检查 ####################
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
echo -e "[ERROR] 需要 root 权限来安装 systemd 单元" echo "[ERROR] root required to install systemd unit" >&2
exit 1 exit 1
fi fi
#################### 目录初始化 #################### install -d -m 0755 "$RUNTIME_DIR" "$LOG_DIR" "$CONF_DIR"
install -d -m 0755 "$Server_Dir/conf" "$Server_Dir/logs" "$Server_Dir/temp" cat >"$UNIT_PATH" <<EOF
# 预创建 env 文件,避免 systemd 因路径不存在报错
: > "$Env_File"
chmod 0644 "$Env_File"
#################### 生成 systemd Unit ####################
cat >"$Unit_Path" <<EOF
[Unit] [Unit]
Description=Clash for Linux (Mihomo) Description=Clash for Linux (Mihomo)
Documentation=https://github.com/wnlen/clash-for-linux Documentation=https://github.com/wnlen/clash-for-linux
@ -40,32 +30,24 @@ StartLimitBurst=10
[Service] [Service]
Type=simple Type=simple
User=$Service_User User=${SERVICE_USER}
Group=$Service_Group Group=${SERVICE_GROUP}
WorkingDirectory=$Server_Dir WorkingDirectory=${PROJECT_DIR}
# 启动环境
Environment=SYSTEMD_MODE=true
Environment=CLASH_ENV_FILE=$Env_File
Environment=HOME=/root Environment=HOME=/root
# 主进程必须由 start.sh 最后一跳 exec 成 mihomo/clash ExecStart=/bin/bash ${PROJECT_DIR}/scripts/run_clash.sh --foreground
ExecStart=/bin/bash $Server_Dir/start.sh ExecStop=/bin/bash ${PROJECT_DIR}/clashctl --from-systemd stop
ExecStop=/bin/bash $Server_Dir/shutdown.sh
ExecReload=/bin/kill -HUP \$MAINPID
# 常驻策略:即使上层脚本正常退出,也要由 systemd 拉回
Restart=always Restart=always
RestartSec=5s RestartSec=5s
# 停止与日志
KillMode=mixed KillMode=mixed
TimeoutStartSec=120 TimeoutStartSec=120
TimeoutStopSec=30 TimeoutStopSec=30
StandardOutput=journal StandardOutput=journal
StandardError=journal StandardError=journal
# 安全与文件权限
UMask=0022 UMask=0022
NoNewPrivileges=false NoNewPrivileges=false
@ -73,13 +55,11 @@ NoNewPrivileges=false
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
#################### 刷新 systemd ####################
systemctl daemon-reload systemctl daemon-reload
systemctl enable "$Service_Name".service >/dev/null 2>&1 || true systemctl enable "${SERVICE_NAME}.service" >/dev/null 2>&1 || true
echo -e "[OK] 已生成 systemd 单元: ${Unit_Path}" echo "[OK] systemd unit installed: ${UNIT_PATH}"
echo -e "已启用开机自启,可执行以下命令启动服务:" echo "start : systemctl start ${SERVICE_NAME}.service"
echo -e " systemctl restart ${Service_Name}.service" echo "stop : systemctl stop ${SERVICE_NAME}.service"
echo -e "查看状态:" echo "restart : systemctl restart ${SERVICE_NAME}.service"
echo -e " systemctl status ${Service_Name}.service -l --no-pager" echo "status : systemctl status ${SERVICE_NAME}.service -l --no-pager"

56
scripts/run_clash.sh Normal file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUNTIME_DIR="$PROJECT_DIR/runtime"
LOG_DIR="$PROJECT_DIR/logs"
CONFIG_FILE="$RUNTIME_DIR/config.yaml"
PID_FILE="$RUNTIME_DIR/clash.pid"
mkdir -p "$RUNTIME_DIR" "$LOG_DIR"
FOREGROUND=false
DAEMON=false
for arg in "$@"; do
case "$arg" in
--foreground) FOREGROUND=true ;;
--daemon) DAEMON=true ;;
*)
echo "[ERROR] Unknown arg: $arg" >&2
exit 2
;;
esac
done
if [ ! -s "$CONFIG_FILE" ]; then
echo "[ERROR] runtime config not found: $CONFIG_FILE" >&2
exit 2
fi
if grep -q '\${' "$CONFIG_FILE"; then
echo "[ERROR] unresolved placeholder found in $CONFIG_FILE" >&2
exit 2
fi
# 这里先沿用你原来的 resolve_clash.sh
# shellcheck disable=SC1091
source "$PROJECT_DIR/scripts/get_cpu_arch.sh"
# shellcheck disable=SC1091
source "$PROJECT_DIR/scripts/resolve_clash.sh"
CLASH_BIN="$(resolve_clash_bin "$PROJECT_DIR" "${CpuArch:-}")"
if [ "$FOREGROUND" = true ]; then
exec "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR"
fi
if [ "$DAEMON" = true ]; then
nohup "$CLASH_BIN" -f "$CONFIG_FILE" -d "$RUNTIME_DIR" >>"$LOG_DIR/clash.log" 2>&1 &
echo $! > "$PID_FILE"
echo "[OK] Clash started in script mode, pid=$(cat "$PID_FILE")"
exit 0
fi
echo "[ERROR] Must specify --foreground or --daemon" >&2
exit 2

80
scripts/service_lib.sh Normal file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUNTIME_DIR="$PROJECT_DIR/runtime"
PID_FILE="$RUNTIME_DIR/clash.pid"
SERVICE_NAME="clash-for-linux.service"
mkdir -p "$RUNTIME_DIR"
has_systemd() {
command -v systemctl >/dev/null 2>&1
}
service_unit_exists() {
has_systemd || return 1
systemctl show "$SERVICE_NAME" -p LoadState --value 2>/dev/null | grep -q '^loaded$'
}
detect_mode() {
if service_unit_exists && systemctl is-active --quiet "$SERVICE_NAME"; then
echo "systemd"
elif is_script_running; then
echo "script"
elif service_unit_exists; then
echo "systemd-installed"
else
echo "none"
fi
}
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
}
start_via_systemd() {
systemctl start "$SERVICE_NAME"
}
stop_via_systemd() {
systemctl stop "$SERVICE_NAME"
}
restart_via_systemd() {
systemctl restart "$SERVICE_NAME"
}
start_via_script() {
if is_script_running; then
echo "[INFO] clash already running (script mode)"
return 0
fi
"$PROJECT_DIR/scripts/run_clash.sh" --daemon
}
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"
sleep 1
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
fi
fi
rm -f "$PID_FILE"
}
restart_via_script() {
stop_via_script || true
start_via_script
}