mirror of
https://github.com/wnlen/clash-for-linux.git
synced 2026-10-10 12:13:04 +08:00
1534 lines
36 KiB
Bash
Executable File
1534 lines
36 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
resolve_project_dir() {
|
||
# 1) 显式指定优先
|
||
if [ -n "${CLASH_INSTALL_DIR:-}" ] && [ -d "${CLASH_INSTALL_DIR:-}" ]; then
|
||
printf '%s\n' "$CLASH_INSTALL_DIR"
|
||
return 0
|
||
fi
|
||
|
||
# 2) 解析脚本真实路径(兼容软链/安装到 /usr/local/bin)
|
||
local src dir
|
||
src="${BASH_SOURCE[0]}"
|
||
|
||
while [ -L "$src" ]; do
|
||
dir="$(cd -P "$(dirname "$src")" && pwd)"
|
||
src="$(readlink "$src")"
|
||
[[ "$src" != /* ]] && src="$dir/$src"
|
||
done
|
||
|
||
dir="$(cd -P "$(dirname "$src")" && pwd)"
|
||
|
||
# 如果 clashctl 就在项目根目录
|
||
if [ -f "$dir/scripts/service_lib.sh" ]; then
|
||
printf '%s\n' "$dir"
|
||
return 0
|
||
fi
|
||
|
||
# 3) 常见安装目录兜底
|
||
for candidate in \
|
||
"/opt/clash-for-linux" \
|
||
"$HOME/clash-for-linux" \
|
||
"/root/clash-for-linux"
|
||
do
|
||
if [ -f "$candidate/scripts/service_lib.sh" ]; then
|
||
printf '%s\n' "$candidate"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
ui_error "无法定位项目目录" >&2
|
||
exit 1
|
||
}
|
||
|
||
PROJECT_DIR="$(resolve_project_dir)"
|
||
SERVICE_NAME="clash-for-linux.service"
|
||
PROFILED_FILE="/etc/profile.d/clash-for-linux.sh"
|
||
ENV_FILE="$PROJECT_DIR/.env"
|
||
RUNTIME_DIR="$PROJECT_DIR/runtime"
|
||
RUNTIME_CONFIG="$RUNTIME_DIR/config.yaml"
|
||
STATE_FILE="$RUNTIME_DIR/state.env"
|
||
LOG_FILE="$PROJECT_DIR/logs/clash.log"
|
||
|
||
# shellcheck disable=SC1091
|
||
source "$PROJECT_DIR/scripts/service_lib.sh"
|
||
# shellcheck source=scripts/ui.sh
|
||
source "$PROJECT_DIR/scripts/ui.sh"
|
||
|
||
log() { printf "%b\n" "$*"; }
|
||
info() { log "\033[36m[INFO]\033[0m $*"; }
|
||
ok() { log "\033[32m[OK]\033[0m $*"; }
|
||
warn() { log "\033[33m[WARN]\033[0m $*"; }
|
||
err() { log "\033[31m[ERROR]\033[0m $*"; }
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法:
|
||
clashctl 命令 [选项]
|
||
|
||
指令:
|
||
on 开启代理
|
||
off 关闭代理
|
||
start 启动 Clash
|
||
stop 停止 Clash
|
||
restart 重启并自动应用当前配置
|
||
status 查看当前状态
|
||
update 更新到最新版本并自动应用配置
|
||
mode 查看当前运行模式(systemd/script/none)
|
||
ui 输出 Dashboard 地址
|
||
secret 输出当前 secret
|
||
doctor 健康检查
|
||
logs [-f] [-n 100] 查看日志
|
||
sub show|update 查看订阅地址 / 输入或更新订阅并立即生效
|
||
tun status|on|off 查看/启用/关闭 Tun
|
||
mixin status|on|off 查看/启用/关闭 Mixin
|
||
|
||
选项:
|
||
--from-systemd 内部使用,避免 stop 递归调用 systemctl
|
||
-h, --help 显示帮助信息
|
||
EOF
|
||
}
|
||
|
||
require_profiled() {
|
||
if [ ! -f "$PROFILED_FILE" ]; then
|
||
err "未安装 Shell 代理快捷命令:$PROFILED_FILE"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
read_runtime_config_value() {
|
||
local key="$1"
|
||
[ -f "$RUNTIME_CONFIG" ] || return 1
|
||
|
||
awk -v k="$key" '
|
||
$0 ~ "^[[:space:]]*" k ":[[:space:]]*" {
|
||
line = $0
|
||
sub("^[[:space:]]*" k ":[[:space:]]*", "", line)
|
||
gsub("\r", "", line)
|
||
|
||
# 去掉首尾引号
|
||
sub(/^"/, "", line)
|
||
sub(/"$/, "", line)
|
||
sub(/^'\''/, "", line)
|
||
sub(/'\''$/, "", line)
|
||
|
||
print line
|
||
exit
|
||
}
|
||
' "$RUNTIME_CONFIG"
|
||
}
|
||
|
||
write_env_bool() {
|
||
local key="$1"
|
||
local value="$2"
|
||
|
||
if [ ! -f "$ENV_FILE" ]; then
|
||
err "未找到 .env: $ENV_FILE"
|
||
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 "已写入环境变量 ${key}=\"${value}\"" >> "$ENV_FILE"
|
||
fi
|
||
}
|
||
|
||
trim_value() {
|
||
local s="${1:-}"
|
||
s="${s#"${s%%[![:space:]]*}"}"
|
||
s="${s%"${s##*[![:space:]]}"}"
|
||
printf '%s' "$s"
|
||
}
|
||
|
||
read_env_value() {
|
||
local key="$1"
|
||
local env_file="$PROJECT_DIR/.env"
|
||
|
||
[ -f "$env_file" ] || return 1
|
||
|
||
sed -nE "s/^[[:space:]]*(export[[:space:]]+)?${key}=['\"]?([^'\"]*)['\"]?.*$/\2/p" "$env_file" | head -n 1
|
||
}
|
||
|
||
write_env_value() {
|
||
local key="$1"
|
||
local value="$2"
|
||
local env_file="$PROJECT_DIR/.env"
|
||
local tmp_file
|
||
|
||
touch "$env_file"
|
||
tmp_file="$(mktemp)"
|
||
|
||
awk -v k="$key" -v v="$value" '
|
||
BEGIN { updated = 0 }
|
||
$0 ~ "^[[:space:]]*(export[[:space:]]+)?" k "=" {
|
||
print "export " k "='\''" v "'\''"
|
||
updated = 1
|
||
next
|
||
}
|
||
{ print }
|
||
END {
|
||
if (!updated) {
|
||
print "export " k "='\''" v "'\''"
|
||
}
|
||
}
|
||
' "$env_file" > "$tmp_file" && mv "$tmp_file" "$env_file"
|
||
}
|
||
|
||
is_valid_subscription_url() {
|
||
local url
|
||
url="$(trim_value "${1:-}")"
|
||
|
||
[ -n "$url" ] || return 1
|
||
|
||
case "$url" in
|
||
http://*|https://*)
|
||
;;
|
||
*)
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
[[ "$url" =~ [[:space:]] ]] && return 1
|
||
[[ "$url" == "-"* ]] && return 1
|
||
|
||
return 0
|
||
}
|
||
|
||
prompt_for_subscription_url() {
|
||
local current_url=""
|
||
local input_url=""
|
||
|
||
current_url="$(read_env_value "CLASH_URL" || true)"
|
||
current_url="$(trim_value "$current_url")"
|
||
|
||
ui_blank
|
||
ui_subheader "订阅配置"
|
||
|
||
if is_valid_subscription_url "$current_url"; then
|
||
printf " 当前订阅: %s\n" "$current_url"
|
||
printf " 直接回车可继续使用当前订阅\n"
|
||
else
|
||
printf " 当前未配置有效订阅,请输入订阅链接\n"
|
||
fi
|
||
|
||
while true; do
|
||
printf " 订阅链接: "
|
||
IFS= read -r input_url
|
||
input_url="$(trim_value "$input_url")"
|
||
|
||
if [ -z "$input_url" ]; then
|
||
if is_valid_subscription_url "$current_url"; then
|
||
input_url="$current_url"
|
||
else
|
||
ui_error "订阅链接不能为空"
|
||
continue
|
||
fi
|
||
fi
|
||
|
||
if ! is_valid_subscription_url "$input_url"; then
|
||
ui_error "订阅链接格式非法,必须以 http:// 或 https:// 开头"
|
||
continue
|
||
fi
|
||
|
||
write_env_value "CLASH_URL" "$input_url"
|
||
export CLASH_URL="$input_url"
|
||
ui_ok "订阅链接已保存"
|
||
return 0
|
||
done
|
||
}
|
||
|
||
ensure_subscription_url_ready() {
|
||
local current_url=""
|
||
current_url="$(read_env_value "CLASH_URL" || true)"
|
||
current_url="$(trim_value "$current_url")"
|
||
|
||
if is_valid_subscription_url "$current_url"; then
|
||
export CLASH_URL="$current_url"
|
||
return 0
|
||
fi
|
||
|
||
prompt_for_subscription_url
|
||
}
|
||
|
||
read_state_value() {
|
||
local key="$1"
|
||
[ -f "$STATE_FILE" ] || return 1
|
||
sed -nE "s/^${key}=(.*)$/\1/p" "$STATE_FILE" | head -n 1
|
||
}
|
||
|
||
command_exists() {
|
||
command -v "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
port_from_controller() {
|
||
local controller
|
||
controller="$(read_runtime_config_value "external-controller" || true)"
|
||
[ -n "${controller:-}" ] || return 1
|
||
printf '%s\n' "${controller##*:}"
|
||
}
|
||
|
||
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
|
||
|
||
return 1
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
has_git_repo() {
|
||
git -C "$PROJECT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1
|
||
}
|
||
|
||
get_current_branch() {
|
||
git -C "$PROJECT_DIR" branch --show-current 2>/dev/null
|
||
}
|
||
|
||
cmd_on() {
|
||
require_profiled
|
||
# shellcheck disable=SC1090
|
||
source "$PROFILED_FILE"
|
||
proxy_on
|
||
}
|
||
|
||
cmd_off() {
|
||
require_profiled
|
||
# shellcheck disable=SC1090
|
||
source "$PROFILED_FILE"
|
||
proxy_off
|
||
}
|
||
cmd_mode() {
|
||
detect_mode
|
||
}
|
||
|
||
generate_config_once() {
|
||
ui_info "正在重新生成配置 ..."
|
||
if ! "$PROJECT_DIR/scripts/generate_config.sh"; then
|
||
ui_error "配置生成失败"
|
||
return 1
|
||
fi
|
||
ui_ok "配置生成成功"
|
||
}
|
||
|
||
cmd_generate() {
|
||
ensure_subscription_url_ready || return 1
|
||
generate_config_once
|
||
}
|
||
|
||
cmd_start() {
|
||
local mode
|
||
mode="$(detect_mode)"
|
||
|
||
case "$mode" in
|
||
systemd|systemd-installed)
|
||
start_via_systemd
|
||
ui_ok "已通过 systemd 启动 Clash"
|
||
;;
|
||
script|none)
|
||
start_via_script
|
||
ui_ok "已通过脚本模式启动 Clash"
|
||
;;
|
||
*)
|
||
err "未知模式: $mode"
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
cmd_stop() {
|
||
local from_systemd="${1:-false}"
|
||
local mode
|
||
mode="$(detect_mode)"
|
||
|
||
case "$mode" in
|
||
systemd)
|
||
if [ "$from_systemd" = "true" ]; then
|
||
ui_ok "来自 systemd 的停止请求,已跳过重复停止操作"
|
||
return 0
|
||
fi
|
||
stop_via_systemd
|
||
ui_ok "已通过 systemd 停止 Clash"
|
||
;;
|
||
systemd-installed)
|
||
ui_info "systemd 服务已安装但未运行"
|
||
;;
|
||
script)
|
||
stop_via_script
|
||
ui_ok "已通过脚本模式停止 Clash"
|
||
;;
|
||
none)
|
||
ui_info "Clash 未运行"
|
||
;;
|
||
*)
|
||
err "未知模式: $mode"
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
show_update_result_summary() {
|
||
[ -n "${LAST_CONFIG_RESULT:-}" ] || return 0
|
||
|
||
echo
|
||
ui_subheader "配置更新结果"
|
||
|
||
if [ "$LAST_CONFIG_RESULT" = "success" ]; then
|
||
echo " 状态 : 已成功更新配置"
|
||
return 0
|
||
fi
|
||
|
||
echo " 状态 : 失败(已沿用旧配置)"
|
||
[ -n "$LAST_CONFIG_REASON" ] && echo " 原因 : $LAST_CONFIG_REASON"
|
||
[ -n "$LAST_CONFIG_HINT" ] && echo " 建议 : $LAST_CONFIG_HINT"
|
||
}
|
||
|
||
safe_regenerate_config() {
|
||
local config_file="$PROJECT_DIR/runtime/config.yaml"
|
||
|
||
LAST_CONFIG_RESULT="success"
|
||
LAST_CONFIG_REASON=""
|
||
LAST_CONFIG_HINT=""
|
||
|
||
ui_info "正在重新生成配置 ..."
|
||
if bash "$PROJECT_DIR/scripts/generate_config.sh" >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
|
||
LAST_CONFIG_RESULT="failed"
|
||
|
||
case "$(read_state_value reason 2>/dev/null || true)" in
|
||
url_invalid)
|
||
LAST_CONFIG_REASON="订阅链接格式错误"
|
||
LAST_CONFIG_HINT="必须以 http:// 或 https:// 开头"
|
||
;;
|
||
url_missing)
|
||
LAST_CONFIG_REASON="未配置订阅链接"
|
||
LAST_CONFIG_HINT="请先设置 CLASH_URL"
|
||
;;
|
||
*)
|
||
LAST_CONFIG_REASON="订阅不可用或配置生成失败"
|
||
LAST_CONFIG_HINT="请检查订阅链接和网络连接"
|
||
;;
|
||
esac
|
||
|
||
if [ -s "$config_file" ]; then
|
||
return 2
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
cmd_update() {
|
||
local resume_mode="${1:-}"
|
||
local branch remote_name dirty pull_ok=1
|
||
|
||
if ! has_git_repo; then
|
||
err "当前目录不是 Git 仓库: $PROJECT_DIR"
|
||
exit 1
|
||
fi
|
||
|
||
branch="$(get_current_branch)"
|
||
if [ -z "${branch:-}" ]; then
|
||
err "无法识别当前分支"
|
||
exit 1
|
||
fi
|
||
|
||
remote_name="origin"
|
||
|
||
# 第二阶段:跳过 git 更新,只执行更新后的收尾逻辑
|
||
if [ "$resume_mode" != "--resume-after-self-update" ]; then
|
||
if [ -f "$PROJECT_DIR/.env" ]; then
|
||
cp -f "$PROJECT_DIR/.env" "$PROJECT_DIR/.env.bak" 2>/dev/null || true
|
||
fi
|
||
|
||
dirty="$(git -C "$PROJECT_DIR" status --porcelain --untracked-files=no)"
|
||
if [ -n "${dirty:-}" ]; then
|
||
ui_warn "检测到本地已修改但未提交的文件,自动切换到安全强制更新模式"
|
||
pull_ok=0
|
||
fi
|
||
|
||
if [ "$pull_ok" -eq 1 ]; then
|
||
ui_info "正在从 ${remote_name}/${branch} 拉取最新代码 ..."
|
||
if ! git -C "$PROJECT_DIR" pull --ff-only "$remote_name" "$branch"; then
|
||
ui_warn "git pull 失败,自动切换到强制更新模式"
|
||
pull_ok=0
|
||
fi
|
||
fi
|
||
|
||
if [ "$pull_ok" -eq 0 ]; then
|
||
ui_info "正在获取 ${remote_name}/${branch} 最新代码 ..."
|
||
if ! git -C "$PROJECT_DIR" fetch "$remote_name" "$branch"; then
|
||
err "git fetch 失败"
|
||
exit 1
|
||
fi
|
||
|
||
ui_warn "正在重置本地代码到 ${remote_name}/${branch}"
|
||
if ! git -C "$PROJECT_DIR" reset --hard "${remote_name}/${branch}"; then
|
||
err "git reset --hard 失败"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
if [ -f "$PROJECT_DIR/.env.bak" ]; then
|
||
mv -f "$PROJECT_DIR/.env.bak" "$PROJECT_DIR/.env" 2>/dev/null || true
|
||
fi
|
||
|
||
hash -r
|
||
|
||
ui_info "已更新到最新代码,正在切换到新版本继续执行..."
|
||
exec "$PROJECT_DIR/clashctl" update --resume-after-self-update
|
||
fi
|
||
|
||
# ===== 从这里开始,一定是新版本脚本在执行 =====
|
||
|
||
ui_info "修复执行权限中..."
|
||
chmod +x "$PROJECT_DIR/clashctl" 2>/dev/null || true
|
||
chmod +x "$PROJECT_DIR"/scripts/*.sh 2>/dev/null || true
|
||
chmod +x "$PROJECT_DIR"/bin/* 2>/dev/null || true
|
||
sed -i 's/\r$//' "$PROJECT_DIR"/scripts/*.sh 2>/dev/null || true
|
||
|
||
ui_info "正在停止旧实例 ..."
|
||
if has_systemd; then
|
||
systemctl stop clash-for-linux.service >/dev/null 2>&1 || true
|
||
else
|
||
"$PROJECT_DIR/scripts/stop_clash.sh" >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
local regen_result=0
|
||
if safe_regenerate_config; then
|
||
regen_result=0
|
||
else
|
||
regen_result=$?
|
||
fi
|
||
|
||
if [ "$regen_result" -eq 1 ]; then
|
||
err "配置生成失败,且不存在可用旧配置"
|
||
exit 1
|
||
fi
|
||
|
||
ui_info "正在启动服务 ..."
|
||
if has_systemd; then
|
||
if ! systemctl start clash-for-linux.service; then
|
||
err "systemd 启动失败"
|
||
exit 1
|
||
fi
|
||
|
||
ui_info "等待服务就绪..."
|
||
if wait_for_systemd_active; then
|
||
if [ "$regen_result" -eq 2 ]; then
|
||
ui_ok "代码已更新,当前继续使用旧配置运行"
|
||
else
|
||
ui_ok "更新完成"
|
||
fi
|
||
cmd_status
|
||
return 0
|
||
fi
|
||
|
||
err "服务启动未就绪"
|
||
systemctl status clash-for-linux.service -l --no-pager || true
|
||
exit 1
|
||
else
|
||
if ! "$PROJECT_DIR/scripts/run_clash.sh" --daemon; then
|
||
err "脚本模式启动失败"
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$regen_result" -eq 2 ]; then
|
||
ui_ok "代码已更新,当前继续使用旧配置运行"
|
||
show_update_result_summary
|
||
else
|
||
ui_ok "更新完成"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
cmd_update_force() {
|
||
local branch remote_name
|
||
local env_file backup_file
|
||
local regen_result=0
|
||
|
||
if ! has_git_repo; then
|
||
err "当前目录不是 Git 仓库: $PROJECT_DIR"
|
||
exit 1
|
||
fi
|
||
|
||
branch="$(get_current_branch)"
|
||
if [ -z "${branch:-}" ]; then
|
||
err "无法识别当前分支"
|
||
exit 1
|
||
fi
|
||
|
||
remote_name="origin"
|
||
env_file="$PROJECT_DIR/.env"
|
||
backup_file="$PROJECT_DIR/runtime/.env.local.backup"
|
||
|
||
mkdir -p "$PROJECT_DIR/runtime"
|
||
|
||
if [ -f "$env_file" ]; then
|
||
ui_info "正在备份本地环境变量覆盖项 ..."
|
||
backup_local_env_overrides "$env_file" "$backup_file"
|
||
fi
|
||
|
||
ui_warn "强制更新:本地代码修改将被覆盖"
|
||
git -C "$PROJECT_DIR" fetch "$remote_name" "$branch" || {
|
||
err "git fetch 失败"
|
||
exit 1
|
||
}
|
||
|
||
git -C "$PROJECT_DIR" reset --hard "${remote_name}/${branch}" || {
|
||
err "git reset --hard 失败"
|
||
exit 1
|
||
}
|
||
|
||
if [ -f "$env_file" ] && [ -f "$backup_file" ]; then
|
||
ui_info "正在恢复本地环境变量 ..."
|
||
restore_local_env_overrides "$env_file" "$backup_file"
|
||
fi
|
||
|
||
ui_info "修复执行权限中..."
|
||
chmod +x "$PROJECT_DIR"/clashctl 2>/dev/null || true
|
||
chmod +x "$PROJECT_DIR"/scripts/*.sh 2>/dev/null || true
|
||
chmod +x "$PROJECT_DIR"/bin/* 2>/dev/null || true
|
||
sed -i 's/\r$//' "$PROJECT_DIR"/scripts/*.sh 2>/dev/null || true
|
||
|
||
regen_result=0
|
||
safe_regenerate_config || regen_result=$?
|
||
|
||
if [ "$regen_result" -eq 1 ]; then
|
||
err "配置生成失败,且不存在可用旧配置"
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$regen_result" -eq 1 ]; then
|
||
err "配置生成失败,且不存在可用旧配置"
|
||
exit 1
|
||
fi
|
||
|
||
ui_info "正在重启服务 ..."
|
||
if has_systemd; then
|
||
systemctl restart clash-for-linux.service || {
|
||
err "systemd 重启失败"
|
||
exit 1
|
||
}
|
||
else
|
||
bash "$PROJECT_DIR/scripts/run_clash.sh" --daemon || {
|
||
err "脚本模式启动失败"
|
||
exit 1
|
||
}
|
||
fi
|
||
|
||
if [ "$regen_result" -eq 2 ]; then
|
||
ui_ok "强制更新完成,当前继续使用旧配置运行"
|
||
else
|
||
ui_ok "强制更新完成"
|
||
fi
|
||
cmd_status
|
||
}
|
||
|
||
# === 修复执行权限 ===
|
||
fix_exec_permissions() {
|
||
local dir="${PROJECT_DIR:-$(pwd)}"
|
||
|
||
chmod +x "$dir"/scripts/*.sh 2>/dev/null || true
|
||
chmod +x "$dir"/bin/* 2>/dev/null || true
|
||
|
||
# 可选:修复 CRLF(防止 Windows 换行)
|
||
sed -i 's/\r$//' "$dir"/scripts/*.sh 2>/dev/null || true
|
||
}
|
||
|
||
# === 等待 service 进入 active ===
|
||
wait_for_service_active() {
|
||
local svc="${1:-clash-for-linux.service}"
|
||
local timeout="${2:-10}"
|
||
|
||
for ((i=0; i<timeout; i++)); do
|
||
state=$(systemctl is-active "$svc" 2>/dev/null || true)
|
||
if [ "$state" = "active" ]; then
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
# === 获取实际端口(从 runtime/config.yaml 解析)===
|
||
get_actual_ports() {
|
||
local cfg="$PROJECT_DIR/runtime/config.yaml"
|
||
|
||
ACTUAL_HTTP_PORT=""
|
||
ACTUAL_CTRL_PORT=""
|
||
|
||
[ -f "$cfg" ] || return
|
||
|
||
# mixed-port
|
||
ACTUAL_HTTP_PORT=$(awk -F': *' '
|
||
/^[[:space:]]*mixed-port:/ {
|
||
gsub("\r","",$2)
|
||
print $2
|
||
exit
|
||
}' "$cfg")
|
||
|
||
# external-controller
|
||
local ctrl
|
||
ctrl=$(awk -F': *' '
|
||
/^[[:space:]]*external-controller:/ {
|
||
gsub("\r","",$2)
|
||
print $2
|
||
exit
|
||
}' "$cfg")
|
||
|
||
if [ -n "$ctrl" ]; then
|
||
ACTUAL_CTRL_PORT="${ctrl##*:}"
|
||
fi
|
||
|
||
export ACTUAL_HTTP_PORT ACTUAL_CTRL_PORT
|
||
}
|
||
|
||
wait_for_systemd_active() {
|
||
local i state
|
||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||
state="$(systemctl is-active "$SERVICE_NAME" 2>/dev/null || true)"
|
||
if [ "$state" = "active" ]; then
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
return 1
|
||
}
|
||
|
||
cmd_restart() {
|
||
ui_info "正在停止 Clash ..."
|
||
cmd_stop "${1:-false}" || true
|
||
sleep 1
|
||
|
||
ui_info "正在重新生成配置 ..."
|
||
if ! cmd_generate; then
|
||
return 1
|
||
fi
|
||
|
||
ui_info "正在启动 Clash ..."
|
||
if ! cmd_start; then
|
||
err "启动失败"
|
||
return 1
|
||
fi
|
||
|
||
if has_systemd; then
|
||
ui_info "正在等待服务启动完成 ..."
|
||
if wait_for_systemd_active; then
|
||
ui_ok "重启完成"
|
||
return 0
|
||
fi
|
||
|
||
err "服务未就绪"
|
||
systemctl status "$SERVICE_NAME" -l --no-pager || true
|
||
return 1
|
||
fi
|
||
|
||
ui_ok "重启完成"
|
||
}
|
||
|
||
get_lan_ip() {
|
||
hostname -I 2>/dev/null | awk '{print $1}' || true
|
||
}
|
||
|
||
cmd_ui() {
|
||
local raw="${1:-}"
|
||
local controller host port secret
|
||
local lan_ip public_ip
|
||
local lan_ui public_ui custom_ui
|
||
local recommended_ui
|
||
|
||
controller="$(read_runtime_config_value "external-controller" || true)"
|
||
[ -n "${controller:-}" ] || controller="127.0.0.1:9090"
|
||
|
||
host="${controller%:*}"
|
||
port="${controller##*:}"
|
||
|
||
secret="$(read_runtime_config_value "secret" || true)"
|
||
|
||
lan_ip="$(get_lan_ip)"
|
||
public_ip="$(curl -fsS --max-time 5 ifconfig.me 2>/dev/null || true)"
|
||
custom_ui="${CLASH_DASHBOARD_PUBLIC_URL:-}"
|
||
|
||
lan_ui=""
|
||
public_ui=""
|
||
|
||
[ -n "${lan_ip:-}" ] && lan_ui="http://${lan_ip}:${port}/ui"
|
||
[ -n "${public_ip:-}" ] && public_ui="http://${public_ip}:${port}/ui"
|
||
|
||
recommended_ui="$public_ui"
|
||
[ -z "${recommended_ui:-}" ] && recommended_ui="$lan_ui"
|
||
[ -z "${recommended_ui:-}" ] && recommended_ui="http://127.0.0.1:${port}/ui"
|
||
|
||
if [ "$raw" = "--raw" ]; then
|
||
printf '%s\n' "$recommended_ui"
|
||
return 0
|
||
fi
|
||
|
||
ui_blank
|
||
ui_summary_begin "😼 Clash Web 控制台"
|
||
ui_summary_row "🌏 公网" "$recommended_ui"
|
||
[ -n "${lan_ui:-}" ] && ui_summary_row "💻 内网访问" "$lan_ui"
|
||
[ -n "${custom_ui:-}" ] && ui_summary_row "🌐 公共访问" "$custom_ui"
|
||
if [ -n "${secret:-}" ]; then
|
||
ui_summary_row "🔑 Web 密钥" "$secret"
|
||
else
|
||
ui_summary_row "🔑 Web 密钥" "未配置"
|
||
fi
|
||
ui_summary_end
|
||
|
||
ui_blank
|
||
ui_subheader "提示"
|
||
printf ' • 若无法访问,请放行端口 %s\n' "$port"
|
||
if [ -n "${secret:-}" ]; then
|
||
printf ' • 首次打开面板后,输入 Web 密钥即可连接\n'
|
||
else
|
||
printf ' • 当前未配置 Web 密钥,可直接访问面板\n'
|
||
fi
|
||
}
|
||
|
||
cmd_secret() {
|
||
local secret
|
||
|
||
if [ ! -s "$RUNTIME_CONFIG" ]; then
|
||
err "未找到运行时配置文件: $RUNTIME_CONFIG"
|
||
echo "请先执行 install.sh 或 clashctl generate" >&2
|
||
exit 1
|
||
fi
|
||
|
||
secret="$(read_runtime_config_value "secret" || true)"
|
||
if [ -z "${secret:-}" ]; then
|
||
err "未找到 secret 在配置: $RUNTIME_CONFIG"
|
||
exit 1
|
||
fi
|
||
|
||
printf '%s\n' "$secret"
|
||
}
|
||
|
||
cmd_sub() {
|
||
local subcmd="${1:-show}"
|
||
|
||
case "$subcmd" in
|
||
show)
|
||
local current_url=""
|
||
current_url="$(read_env_value "CLASH_URL" || true)"
|
||
|
||
if is_valid_subscription_url "$current_url"; then
|
||
echo "$current_url"
|
||
else
|
||
echo "未配置订阅"
|
||
fi
|
||
;;
|
||
|
||
update)
|
||
ensure_subscription_url_ready || return 1
|
||
|
||
ui_info "正在停止 Clash ..."
|
||
cmd_stop || true
|
||
|
||
generate_config_once || return 1
|
||
|
||
ui_info "正在启动 Clash ..."
|
||
cmd_start || return 1
|
||
|
||
ui_ok "订阅已更新并应用"
|
||
;;
|
||
|
||
*)
|
||
err "未知 sub 子命令: $subcmd"
|
||
echo "用法: clashctl sub [show|update]"
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
cmd_tun() {
|
||
local subcmd="${1:-status}"
|
||
|
||
case "$subcmd" in
|
||
status)
|
||
if [ -f "$ENV_FILE" ]; then
|
||
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_TUN=' "$ENV_FILE" || echo 'CLASH_TUN=未配置'
|
||
else
|
||
err "未找到 .env"
|
||
exit 1
|
||
fi
|
||
;;
|
||
on)
|
||
write_env_bool "CLASH_TUN" "true"
|
||
ui_ok "已写入 CLASH_TUN=true"
|
||
;;
|
||
off)
|
||
write_env_bool "CLASH_TUN" "false"
|
||
ui_ok "已写入 CLASH_TUN=false"
|
||
;;
|
||
*)
|
||
err "未知 tun 子命令: $subcmd"
|
||
echo "用法: clashctl tun [status|on|off]"
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
cmd_mixin() {
|
||
local subcmd="${1:-status}"
|
||
|
||
case "$subcmd" in
|
||
status)
|
||
if [ -f "$ENV_FILE" ]; then
|
||
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_MIXIN=' "$ENV_FILE" || echo 'CLASH_MIXIN=未配置'
|
||
else
|
||
err "未找到 .env"
|
||
exit 1
|
||
fi
|
||
;;
|
||
on)
|
||
write_env_bool "CLASH_MIXIN" "true"
|
||
ui_ok "已写入 CLASH_MIXIN=true"
|
||
;;
|
||
off)
|
||
write_env_bool "CLASH_MIXIN" "false"
|
||
ui_ok "已写入 CLASH_MIXIN=false"
|
||
;;
|
||
*)
|
||
err "未知 mixin 子命令: $subcmd"
|
||
echo "用法: clashctl mixin [status|on|off]"
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
cmd_status() {
|
||
local pid="-"
|
||
local running_text="未运行"
|
||
local service_active_text="-"
|
||
local service_enabled_text="-"
|
||
|
||
local proxy_port="-"
|
||
local dashboard_port="-"
|
||
local controller_addr="-"
|
||
local dashboard_url="-"
|
||
|
||
local secret_display="未配置"
|
||
local secret_value=""
|
||
|
||
mode="$(detect_mode)"
|
||
|
||
case "$mode" in
|
||
systemd)
|
||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||
running_text="运行中"
|
||
else
|
||
running_text="未运行"
|
||
fi
|
||
|
||
local active_raw=""
|
||
active_raw="$(systemctl is-active "$SERVICE_NAME" 2>/dev/null || true)"
|
||
case "$active_raw" in
|
||
active) service_active_text="运行中" ;;
|
||
inactive) service_active_text="未运行" ;;
|
||
failed) service_active_text="失败" ;;
|
||
activating) service_active_text="启动中" ;;
|
||
deactivating) service_active_text="停止中" ;;
|
||
*) service_active_text="${active_raw:--}" ;;
|
||
esac
|
||
|
||
local enabled_raw=""
|
||
enabled_raw="$(systemctl is-enabled "$SERVICE_NAME" 2>/dev/null || true)"
|
||
case "$enabled_raw" in
|
||
enabled) service_enabled_text="已开启" ;;
|
||
disabled) service_enabled_text="未开启" ;;
|
||
static) service_enabled_text="静态" ;;
|
||
indirect) service_enabled_text="间接" ;;
|
||
masked) service_enabled_text="已屏蔽" ;;
|
||
*) service_enabled_text="${enabled_raw:--}" ;;
|
||
esac
|
||
|
||
pid="$(systemctl show -p MainPID --value "$SERVICE_NAME" 2>/dev/null || true)"
|
||
[ -z "${pid:-}" ] && pid="-"
|
||
[ "$pid" = "0" ] && pid="-"
|
||
;;
|
||
systemd-installed)
|
||
service_type="systemd"
|
||
running="未运行"
|
||
service_active="$(systemctl is-active "$SERVICE_NAME" 2>/dev/null || true)"
|
||
service_enabled="$(systemctl is-enabled "$SERVICE_NAME" 2>/dev/null || true)"
|
||
;;
|
||
script)
|
||
if [ -n "${pid:-}" ] && kill -0 "$pid" 2>/dev/null; then
|
||
running_text="运行中"
|
||
else
|
||
running_text="未运行"
|
||
pid="-"
|
||
fi
|
||
service_active_text="-"
|
||
service_enabled_text="-"
|
||
;;
|
||
|
||
none)
|
||
running_text="未运行"
|
||
service_active_text="-"
|
||
service_enabled_text="-"
|
||
pid="-"
|
||
;;
|
||
esac
|
||
|
||
[ -f "$RUNTIME_CONFIG" ] && config_exists="是"
|
||
[ -f "$STATE_FILE" ] && state_exists="是"
|
||
|
||
gen_status="$(read_state_value LAST_GENERATE_STATUS || true)"
|
||
gen_reason="$(read_state_value LAST_GENERATE_REASON || true)"
|
||
cfg_source="$(read_state_value LAST_CONFIG_SOURCE || true)"
|
||
gen_time="$(read_state_value LAST_GENERATE_AT || true)"
|
||
|
||
run_result="$(read_state_value LAST_RUN_STATUS || true)"
|
||
run_mode="$(read_state_value LAST_RUN_MODE || true)"
|
||
run_pid="$(read_state_value LAST_RUN_PID || true)"
|
||
start_time="$(read_state_value LAST_RUN_AT || true)"
|
||
|
||
http_port="$(http_port_from_config || echo "-")"
|
||
dashboard_port="$(port_from_controller || echo "-")"
|
||
controller="$(read_runtime_config_value "external-controller" || echo "-")"
|
||
dashboard_url="$(cmd_ui --raw 2>/dev/null || echo "-")"
|
||
|
||
secret_value="$(read_runtime_config_value "secret" 2>/dev/null || true)"
|
||
|
||
if [ -z "${secret_value:-}" ] && [ -f "$PROJECT_DIR/.env" ]; then
|
||
secret_value="$(sed -nE "s/^[[:space:]]*(export[[:space:]]+)?CLASH_SECRET=['\"]?([^'\"]+)['\"]?.*$/\2/p" "$PROJECT_DIR/.env" | head -n 1)"
|
||
fi
|
||
|
||
[ -n "${secret_value:-}" ] && secret_display="$secret_value"
|
||
|
||
proxy_port="$(read_runtime_config_value "mixed-port" || true)"
|
||
[ -z "$proxy_port" ] && proxy_port="$(read_runtime_config_value "port" || true)"
|
||
[ -z "$proxy_port" ] && proxy_port="-"
|
||
|
||
controller_addr="$(read_runtime_config_value "external-controller" || true)"
|
||
[ -z "$controller_addr" ] && controller_addr="-"
|
||
|
||
ui_summary_begin "运行与服务"
|
||
ui_summary_row "运行模式" "$mode"
|
||
ui_summary_row "运行状态" "$running_text"
|
||
ui_summary_row "服务状态" "$service_active_text"
|
||
ui_summary_row "开机自启" "$service_enabled_text"
|
||
ui_summary_row "进程 ID" "${pid:--}"
|
||
ui_summary_end
|
||
|
||
ui_blank
|
||
ui_summary_begin "端口与面板"
|
||
ui_summary_row "面板地址" "$dashboard_url"
|
||
ui_summary_row "密钥配置" "$secret_display"
|
||
ui_summary_row "代理端口" "$proxy_port"
|
||
ui_summary_row "面板端口" "$dashboard_port"
|
||
ui_summary_row "控制地址" "$controller_addr"
|
||
ui_summary_end
|
||
}
|
||
|
||
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" "$*"
|
||
}
|
||
|
||
cmd_doctor() {
|
||
local mode running="未运行"
|
||
local failed=0
|
||
local warned=0
|
||
|
||
local controller="-"
|
||
local dashboard_url="-"
|
||
local dashboard_port="-"
|
||
local http_port="-"
|
||
local secret=""
|
||
local last_generate_status="-"
|
||
local last_generate_reason="-"
|
||
local last_config_source="-"
|
||
|
||
mode="$(detect_mode)"
|
||
|
||
ui_header "clashctl 诊断"
|
||
|
||
ui_info "基础信息"
|
||
ui_info "项目路径:$PROJECT_DIR"
|
||
ui_info "运行模式:$mode"
|
||
ui_info "配置文件:$RUNTIME_CONFIG"
|
||
|
||
ui_blank
|
||
ui_info "[检查] 运行配置"
|
||
if [ -s "$RUNTIME_CONFIG" ]; then
|
||
ui_ok "运行配置文件存在"
|
||
else
|
||
ui_error "运行配置文件不存在或为空"
|
||
failed=1
|
||
fi
|
||
|
||
if [ -f "$RUNTIME_CONFIG" ]; then
|
||
if grep -q '\${' "$RUNTIME_CONFIG"; then
|
||
ui_error "配置文件存在未替换变量"
|
||
failed=1
|
||
else
|
||
ui_ok "配置文件检查通过"
|
||
fi
|
||
fi
|
||
|
||
ui_blank
|
||
ui_info "状态文件"
|
||
if [ -f "$STATE_FILE" ]; then
|
||
ui_ok "状态文件存在"
|
||
|
||
last_generate_status="$(read_state_value LAST_GENERATE_STATUS || echo "-")"
|
||
last_generate_reason="$(read_state_value LAST_GENERATE_REASON || echo "-")"
|
||
last_config_source="$(read_state_value LAST_CONFIG_SOURCE || echo "-")"
|
||
|
||
if [ "$last_generate_status" = "success" ]; then
|
||
ui_ok "最近配置生成成功"
|
||
elif [ "$last_generate_status" != "-" ]; then
|
||
ui_warn "最近配置生成状态:$last_generate_status"
|
||
warned=1
|
||
else
|
||
ui_warn "状态文件存在但生成状态缺失"
|
||
warned=1
|
||
fi
|
||
|
||
[ "$last_generate_reason" != "-" ] && ui_info "生成原因:$last_generate_reason"
|
||
[ "$last_config_source" != "-" ] && ui_info "配置来源:$last_config_source"
|
||
else
|
||
ui_warn "状态文件缺失"
|
||
warned=1
|
||
fi
|
||
|
||
ui_blank
|
||
ui_info "服务运行"
|
||
case "$mode" in
|
||
systemd)
|
||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||
running="运行中"
|
||
ui_ok "systemd 服务运行正常"
|
||
else
|
||
ui_error "systemd 服务已安装但未运行"
|
||
failed=1
|
||
fi
|
||
;;
|
||
script)
|
||
if is_script_running; then
|
||
running="运行中"
|
||
ui_ok "脚本模式进程正在运行"
|
||
else
|
||
ui_error "脚本模式进程未运行"
|
||
failed=1
|
||
fi
|
||
;;
|
||
systemd-installed)
|
||
ui_warn "systemd 服务已安装但未运行"
|
||
warned=1
|
||
;;
|
||
none)
|
||
ui_warn "未检测到运行中的实例"
|
||
warned=1
|
||
;;
|
||
*)
|
||
ui_error "未知运行模式:$mode"
|
||
failed=1
|
||
;;
|
||
esac
|
||
|
||
ui_blank
|
||
ui_info "控制面板"
|
||
dashboard_url="$(cmd_ui --raw 2>/dev/null || echo "-")"
|
||
controller="$(read_runtime_config_value "external-controller" || echo "-")"
|
||
dashboard_port="$(port_from_controller || echo "-")"
|
||
|
||
if [ "$controller" != "-" ]; then
|
||
ui_ok "控制地址已配置:$controller"
|
||
else
|
||
ui_warn "未配置 external-controller,将使用默认值"
|
||
warned=1
|
||
fi
|
||
|
||
if [ "$dashboard_url" != "-" ]; then
|
||
ui_ok "面板地址可解析"
|
||
else
|
||
ui_warn "无法获取面板地址"
|
||
warned=1
|
||
fi
|
||
|
||
ui_blank
|
||
ui_info "端口监听"
|
||
http_port="$(http_port_from_config || echo "-")"
|
||
if [ "$http_port" != "-" ]; then
|
||
if check_port_listening "$http_port"; then
|
||
ui_ok "代理端口监听正常:$http_port"
|
||
else
|
||
rc=$?
|
||
if [ "$rc" -eq 2 ]; then
|
||
ui_warn "无法检测代理端口(缺少 ss/netstat)"
|
||
warned=1
|
||
else
|
||
if [ "$running" = "运行中" ]; then
|
||
ui_error "代理端口未监听:$http_port"
|
||
failed=1
|
||
else
|
||
ui_warn "代理端口未监听:$http_port"
|
||
warned=1
|
||
fi
|
||
fi
|
||
fi
|
||
else
|
||
ui_warn "无法解析代理端口"
|
||
warned=1
|
||
fi
|
||
|
||
if [ "$dashboard_port" != "-" ]; then
|
||
if check_port_listening "$dashboard_port"; then
|
||
ui_ok "面板端口监听正常:$dashboard_port"
|
||
else
|
||
rc=$?
|
||
if [ "$rc" -eq 2 ]; then
|
||
ui_warn "无法检测面板端口(缺少 ss/netstat)"
|
||
warned=1
|
||
else
|
||
if [ "$running" = "运行中" ]; then
|
||
ui_error "面板端口未监听:$dashboard_port"
|
||
failed=1
|
||
else
|
||
ui_warn "面板端口未监听:$dashboard_port"
|
||
warned=1
|
||
fi
|
||
fi
|
||
fi
|
||
else
|
||
ui_warn "无法解析面板端口"
|
||
warned=1
|
||
fi
|
||
|
||
ui_blank
|
||
ui_info "面板访问"
|
||
if [ "$dashboard_url" != "-" ]; then
|
||
if check_dashboard_http "$dashboard_url"; then
|
||
ui_ok "面板访问正常"
|
||
else
|
||
rc=$?
|
||
if [ "$rc" -eq 2 ]; then
|
||
ui_warn "无法检测面板 HTTP(缺少 curl/wget)"
|
||
warned=1
|
||
else
|
||
ui_warn "面板无法访问"
|
||
warned=1
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
ui_blank
|
||
ui_info "其他组件"
|
||
secret="$(read_runtime_config_value "secret" || true)"
|
||
if [ -n "${secret:-}" ]; then
|
||
ui_ok "已配置 Web 密钥"
|
||
else
|
||
ui_warn "未配置 Web 密钥"
|
||
warned=1
|
||
fi
|
||
|
||
if command_exists clashctl; then
|
||
ui_ok "clashctl 命令可用"
|
||
else
|
||
ui_warn "未在 PATH 中找到 clashctl"
|
||
warned=1
|
||
fi
|
||
|
||
if [ -f "$PROFILED_FILE" ]; then
|
||
ui_ok "Shell 代理辅助脚本存在"
|
||
else
|
||
ui_warn "Shell 代理辅助脚本缺失"
|
||
warned=1
|
||
fi
|
||
|
||
ui_blank
|
||
ui_summary_begin "诊断结果"
|
||
if [ "$failed" -ne 0 ]; then
|
||
ui_summary_row "状态" "失败"
|
||
ui_summary_row "说明" "存在必须修复的问题"
|
||
ui_summary_end
|
||
|
||
ui_next \
|
||
"查看状态:clashctl status" \
|
||
"重启服务:clashctl restart"
|
||
|
||
ui_debug_block \
|
||
"journalctl -u $SERVICE_NAME -e --no-pager" \
|
||
"cat $RUNTIME_CONFIG"
|
||
|
||
return 1
|
||
fi
|
||
|
||
if [ "$warned" -ne 0 ]; then
|
||
ui_summary_row "状态" "警告"
|
||
ui_summary_row "说明" "可运行,但存在风险项"
|
||
ui_summary_end
|
||
return 0
|
||
fi
|
||
|
||
ui_summary_row "状态" "健康"
|
||
ui_summary_row "说明" "检查全部通过"
|
||
ui_summary_end
|
||
return 0
|
||
}
|
||
|
||
cmd_logs() {
|
||
shift || true
|
||
|
||
local follow="false"
|
||
local lines="50"
|
||
local mode
|
||
local arg
|
||
|
||
while [ $# -gt 0 ]; do
|
||
arg="$1"
|
||
case "$arg" in
|
||
-f|--follow)
|
||
follow="true"
|
||
;;
|
||
-n|--lines)
|
||
shift || true
|
||
if [ $# -eq 0 ]; then
|
||
err "logs: -n/--lines 需要一个数字参数"
|
||
exit 1
|
||
fi
|
||
lines="$1"
|
||
;;
|
||
*)
|
||
err "未知 logs 参数: $arg"
|
||
echo "用法: clashctl logs [-f] [-n 100]"
|
||
exit 1
|
||
;;
|
||
esac
|
||
shift || true
|
||
done
|
||
|
||
mode="$(detect_mode)"
|
||
|
||
case "$mode" in
|
||
systemd|systemd-installed)
|
||
if ! command -v journalctl >/dev/null 2>&1; then
|
||
err "未找到 journalctl,无法读取 systemd 日志"
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$follow" = "true" ]; then
|
||
journalctl -u "$SERVICE_NAME" -n "$lines" -f
|
||
else
|
||
journalctl -u "$SERVICE_NAME" -n "$lines" --no-pager
|
||
fi
|
||
;;
|
||
script|none)
|
||
if [ ! -f "$LOG_FILE" ]; then
|
||
warn "未找到日志文件: $LOG_FILE"
|
||
exit 0
|
||
fi
|
||
|
||
if [ "$follow" = "true" ]; then
|
||
tail -n "$lines" -f "$LOG_FILE"
|
||
else
|
||
tail -n "$lines" "$LOG_FILE"
|
||
fi
|
||
;;
|
||
*)
|
||
err "未知模式: $mode"
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
read_env_kv() {
|
||
local env_file="$1"
|
||
local key="$2"
|
||
|
||
[ -f "$env_file" ] || return 0
|
||
sed -nE "s/^[[:space:]]*(export[[:space:]]+)?${key}=['\"]?([^'\"]*)['\"]?$/\2/p" "$env_file" | head -n 1
|
||
}
|
||
|
||
write_env_kv() {
|
||
local env_file="$1"
|
||
local key="$2"
|
||
local value="$3"
|
||
local escaped="${value//\\/\\\\}"
|
||
escaped="${escaped//&/\\&}"
|
||
escaped="${escaped//|/\\|}"
|
||
escaped="${escaped//\'/\'\\\'\'}"
|
||
|
||
if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$env_file"; then
|
||
sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*$|export ${key}='${escaped}'|g" "$env_file"
|
||
else
|
||
printf "export %s='%s'\n" "$key" "$value" >> "$env_file"
|
||
fi
|
||
}
|
||
|
||
backup_local_env_overrides() {
|
||
local env_file="$1"
|
||
local backup_file="$2"
|
||
|
||
: > "$backup_file"
|
||
|
||
local keys=(
|
||
CLASH_URL
|
||
CLASH_SECRET
|
||
CLASH_HTTP_PORT
|
||
CLASH_SOCKS_PORT
|
||
CLASH_REDIR_PORT
|
||
CLASH_LISTEN_IP
|
||
CLASH_ALLOW_LAN
|
||
EXTERNAL_CONTROLLER_ENABLED
|
||
EXTERNAL_CONTROLLER
|
||
ALLOW_INSECURE_TLS
|
||
CLASH_AUTO_UPDATE
|
||
CLASH_DOWNLOAD_URL_TEMPLATE
|
||
)
|
||
|
||
local key value
|
||
for key in "${keys[@]}"; do
|
||
value="$(read_env_kv "$env_file" "$key")"
|
||
if [ -n "${value:-}" ]; then
|
||
printf "%s=%s\n" "$key" "$value" >> "$backup_file"
|
||
fi
|
||
done
|
||
}
|
||
|
||
restore_local_env_overrides() {
|
||
local env_file="$1"
|
||
local backup_file="$2"
|
||
|
||
[ -f "$backup_file" ] || return 0
|
||
|
||
local line key value
|
||
while IFS= read -r line; do
|
||
[ -n "$line" ] || continue
|
||
key="${line%%=*}"
|
||
value="${line#*=}"
|
||
write_env_kv "$env_file" "$key" "$value"
|
||
done < "$backup_file"
|
||
}
|
||
|
||
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 "$from_systemd"
|
||
;;
|
||
status)
|
||
cmd_status
|
||
;;
|
||
update)
|
||
shift
|
||
cmd_update "$@"
|
||
;;
|
||
update-force)
|
||
shift
|
||
cmd_update_force "$@"
|
||
;;
|
||
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
|
||
;;
|
||
logs)
|
||
cmd_logs "$@"
|
||
;;
|
||
""|-h|--help)
|
||
usage
|
||
;;
|
||
*)
|
||
err "未知命令: ${1:-}"
|
||
usage
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
main "$@" |