#!/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