PMFlow/scripts/tunnel_frontend_watch.sh

341 lines
9.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -uo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PROJECT_DIR"
mkdir -p logs
PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH:-}"
URL_PATTERN='https://[a-z0-9-]+\.trycloudflare\.com'
WATCH_LOG="logs/tunnel-frontend-watch.log"
WATCH_STDOUT_LOG="logs/tunnel-frontend-watch.stdout"
WATCH_PID_FILE="logs/tunnel-frontend-watch.pid"
RUN_LOG="logs/tunnel-frontend.log"
TUNNEL_PID_FILE="logs/tunnel-frontend.pid"
PUBLIC_URL_FILE="logs/public_url.txt"
ACTIVE_CHILD_PID=""
is_pid_running() {
local pid="${1:-}"
[[ -n "$pid" ]] || return 1
ps -p "$pid" >/dev/null 2>&1
}
log_watch() {
local message="$1"
local line="[$(date '+%Y-%m-%d %H:%M:%S')] [watch] ${message}"
printf '%s\n' "$line" | tee -a "$WATCH_LOG"
}
log_watch_err() {
local message="$1"
local line="[$(date '+%Y-%m-%d %H:%M:%S')] [watch] ${message}"
printf '%s\n' "$line" | tee -a "$WATCH_LOG" >&2
}
read_frontend_url() {
local line=""
line="$(rg '^frontend=' "$PUBLIC_URL_FILE" 2>/dev/null | tail -n 1 || true)"
if [[ -n "$line" ]]; then
echo "${line#frontend=}"
return 0
fi
sed -n '1p' "$PUBLIC_URL_FILE" 2>/dev/null | rg '^https?://' -N || true
}
read_backend_url() {
local line=""
line="$(rg '^backend=' "$PUBLIC_URL_FILE" 2>/dev/null | tail -n 1 || true)"
if [[ -n "$line" ]]; then
echo "${line#backend=}"
return 0
fi
sed -n '2p' "$PUBLIC_URL_FILE" 2>/dev/null | rg '^https?://' -N || true
}
write_frontend_url() {
local frontend_url="$1"
local backend_url=""
backend_url="$(read_backend_url)"
: > "$PUBLIC_URL_FILE"
echo "frontend=$frontend_url" >> "$PUBLIC_URL_FILE"
if [[ -n "$backend_url" ]]; then
echo "backend=$backend_url" >> "$PUBLIC_URL_FILE"
fi
}
find_cloudflared_bin() {
local bin
bin="$(command -v cloudflared || true)"
if [[ -n "$bin" ]]; then
echo "$bin"
return 0
fi
for candidate in /opt/homebrew/bin/cloudflared /usr/local/bin/cloudflared; do
if [[ -x "$candidate" ]]; then
echo "$candidate"
return 0
fi
done
echo ""
}
extract_tunnel_url() {
local logfile="$1"
rg -o "$URL_PATTERN" "$logfile" 2>/dev/null \
| rg -v "^https://api\\.trycloudflare\\.com$" \
| tail -n 1
}
probe_local_origin() {
local port="$1"
local code=""
code="$(curl --noproxy '*' -s -m 3 -o /dev/null -w "%{http_code}" "http://127.0.0.1:${port}/" || true)"
[[ "$code" =~ ^[0-9]{3}$ ]] || return 1
[[ "$code" -ge 200 && "$code" -lt 500 ]]
}
probe_tunnel_url() {
local url="$1"
local code=""
code="$(curl --noproxy '*' -s -m 8 -o /dev/null -w "%{http_code}" "${url}/" || true)"
[[ "$code" =~ ^[0-9]{3}$ ]] || return 1
[[ "$code" -ge 200 && "$code" -lt 500 ]]
}
probe_tunnel_url_stable() {
local url="$1"
local checks="${2:-2}"
local current=1
while [[ "$current" -le "$checks" ]]; do
probe_tunnel_url "$url" || return 1
if [[ "$current" -lt "$checks" ]]; then
sleep 1
fi
current=$((current + 1))
done
}
resolve_frontend_port() {
local preferred="${PMFLOW_FRONTEND_TUNNEL_PORT:-${FRONTEND_PORT:-5174}}"
local candidates=("$preferred" 5174 5175 5176 5177 5178)
local seen=" "
local port=""
for port in "${candidates[@]}"; do
[[ "$seen" == *" ${port} "* ]] && continue
seen="${seen}${port} "
if probe_local_origin "$port"; then
echo "$port"
return 0
fi
done
echo "$preferred"
}
is_watch_running() {
if [[ ! -f "$WATCH_PID_FILE" ]]; then
return 1
fi
local pid
pid="$(cat "$WATCH_PID_FILE" 2>/dev/null || true)"
if [[ -z "$pid" ]]; then
return 1
fi
if ps -p "$pid" >/dev/null 2>&1; then
return 0
fi
return 1
}
cleanup_children() {
if [[ -n "${ACTIVE_CHILD_PID:-}" ]] && is_pid_running "$ACTIVE_CHILD_PID"; then
kill "$ACTIVE_CHILD_PID" >/dev/null 2>&1 || true
fi
if [[ -f "$TUNNEL_PID_FILE" ]]; then
local pid
pid="$(cat "$TUNNEL_PID_FILE" 2>/dev/null || true)"
if [[ -n "$pid" ]] && is_pid_running "$pid"; then
kill "$pid" >/dev/null 2>&1 || true
fi
fi
}
stop_watch() {
local stopped=0
if [[ -f "$WATCH_PID_FILE" ]]; then
local wpid
wpid="$(cat "$WATCH_PID_FILE" 2>/dev/null || true)"
if [[ -n "$wpid" ]] && is_pid_running "$wpid"; then
kill "$wpid" >/dev/null 2>&1 || true
stopped=1
fi
fi
cleanup_children
rm -f "$WATCH_PID_FILE" "$TUNNEL_PID_FILE"
if [[ "$stopped" -eq 1 ]]; then
echo "[前端隧道守护] 已停止"
else
echo "[前端隧道守护] 未运行"
fi
}
print_status() {
local tunnel_pid=""
if [[ -f "$TUNNEL_PID_FILE" ]]; then
tunnel_pid="$(cat "$TUNNEL_PID_FILE" 2>/dev/null || true)"
fi
if is_watch_running; then
local wpid cpid
wpid="$(cat "$WATCH_PID_FILE" 2>/dev/null || true)"
cpid="${tunnel_pid:-$(cat "$TUNNEL_PID_FILE" 2>/dev/null || true)}"
echo "[前端隧道守护] 运行中 watch_pid=${wpid:-unknown} tunnel_pid=${cpid:-unknown}"
else
echo "[前端隧道守护] 未运行"
fi
local frontend_url backend_url
frontend_url="$(read_frontend_url)"
backend_url="$(read_backend_url)"
if [[ -n "$frontend_url" ]]; then
echo "前端地址: $frontend_url"
if is_pid_running "$tunnel_pid"; then
echo "前端健康: tunnel进程运行中"
else
echo "前端健康: tunnel进程未运行"
fi
else
echo "前端地址: (空)"
fi
if [[ -n "$backend_url" ]]; then
echo "后端地址: $backend_url"
else
echo "后端地址: (未配置/未暴露)"
fi
}
start_watch_background() {
if is_watch_running; then
print_status
return 0
fi
nohup bash "$0" run >"$WATCH_STDOUT_LOG" 2>&1 &
local pid="$!"
echo "$pid" >"$WATCH_PID_FILE"
echo "[前端隧道守护] 已启动 pid=$pid"
sleep 1
tail -n 5 "$WATCH_STDOUT_LOG" 2>/dev/null || true
}
run_watch_loop() {
local cf_bin
cf_bin="$(find_cloudflared_bin)"
if [[ -z "$cf_bin" ]]; then
log_watch_err "cloudflared_not_found"
exit 1
fi
echo "$$" >"$WATCH_PID_FILE"
log_watch "start bin=$cf_bin"
trap 'cleanup_children; rm -f "$WATCH_PID_FILE"; log_watch "exit code=$?"' EXIT INT TERM
while true; do
local frontend_port tunnel_protocol child_pid published current_url url
local strict_public_health public_health_interval public_health_failure_threshold
local unhealthy_count last_health_check_at
frontend_port="$(resolve_frontend_port)"
tunnel_protocol="${PMFLOW_TUNNEL_PROTOCOL_FRONTEND:-${PMFLOW_TUNNEL_PROTOCOL:-http2}}"
strict_public_health="${PMFLOW_TUNNEL_STRICT_PUBLIC_HEALTH:-0}"
public_health_interval="${PMFLOW_TUNNEL_PUBLIC_HEALTH_INTERVAL_SECONDS:-30}"
public_health_failure_threshold="${PMFLOW_TUNNEL_PUBLIC_HEALTH_FAILURE_THRESHOLD:-5}"
: > "$RUN_LOG"
log_watch "origin=http://127.0.0.1:${frontend_port} protocol=${tunnel_protocol}"
env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \
NO_PROXY="*" \
"$cf_bin" tunnel --protocol "$tunnel_protocol" --url "http://127.0.0.1:${frontend_port}" >"$RUN_LOG" 2>&1 &
child_pid="$!"
ACTIVE_CHILD_PID="$child_pid"
echo "$child_pid" > "$TUNNEL_PID_FILE"
published="0"
current_url=""
for _ in {1..90}; do
url="$(extract_tunnel_url "$RUN_LOG" || true)"
if [[ -n "$url" ]]; then
if [[ "$strict_public_health" == "1" ]] && ! probe_tunnel_url_stable "$url"; then
:
else
if [[ "$url" != "$current_url" ]]; then
write_frontend_url "$url"
log_watch "url=$url"
fi
current_url="$url"
published="1"
break
fi
fi
if ! is_pid_running "$child_pid"; then
break
fi
sleep 1
done
unhealthy_count=0
last_health_check_at=0
while is_pid_running "$child_pid"; do
if [[ "$strict_public_health" == "1" && "$published" == "1" && -n "$current_url" ]]; then
if (( $(date +%s) - last_health_check_at >= public_health_interval )); then
last_health_check_at="$(date +%s)"
if probe_tunnel_url "$current_url"; then
unhealthy_count=0
else
unhealthy_count=$((unhealthy_count + 1))
log_watch_err "health_check_failed url=${current_url} count=${unhealthy_count}"
fi
if [[ "$unhealthy_count" -ge "$public_health_failure_threshold" ]]; then
log_watch_err "url_unhealthy_restart url=${current_url}"
kill "$child_pid" >/dev/null 2>&1 || true
break
fi
fi
fi
sleep 5
done
wait "$child_pid" || true
ACTIVE_CHILD_PID=""
if [[ "$published" != "1" ]]; then
log_watch_err "exited_before_url"
else
log_watch_err "exited_restart"
fi
sleep 2
done
}
cmd="${1:-run}"
case "$cmd" in
run)
run_watch_loop
;;
start)
start_watch_background
;;
stop)
stop_watch
;;
status)
print_status
;;
url)
front="$(read_frontend_url)"
back="$(read_backend_url)"
echo "前端地址: ${front:-未就绪}"
echo "后端地址: ${back:-未配置/未暴露}"
;;
*)
echo "Usage: $0 {run|start|stop|status|url}"
exit 1
;;
esac