#!/usr/bin/env bash # ────────────────────────────────────────────────────────────────────────── # mode_procedure 查看服务 + Cloudflare 隧道 一键管理脚本 # # ./share.sh start 启动 server.py(后台) + cloudflared 隧道(后台,http2),打印公网链接 # ./share.sh stop 停掉两者 # ./share.sh restart 重启两者(换新链接) # ./share.sh status 查看运行状态 + 当前链接 # ./share.sh url 只打印当前公网链接 # # 设计要点: # - 两个进程都用 nohup/setsid 脱离终端,关掉终端窗口也不会被 SIGHUP 杀掉。 # - cloudflared 强制 --protocol http2,绕开公司网/VPN 常封的 UDP(QUIC)。 # - 幂等:重复 start 会先清掉旧进程,不会起一堆。 # ────────────────────────────────────────────────────────────────────────── set -euo pipefail # 项目目录 = 本脚本所在目录(shell/)的上一级 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJ_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" PORT="${PORT:-8771}" SERVER_LOG="$PROJ_DIR/.server.log" CF_LOG="$PROJ_DIR/.cloudflared.log" SERVER_PIDF="$PROJ_DIR/.server.pid" CF_PIDF="$PROJ_DIR/.cloudflared.pid" PY="${PYTHON:-python}" # 选一个能后台脱离的方式:优先 setsid,macOS 没有就用 nohup _spawn() { # _spawn local log="$1"; shift if command -v setsid >/dev/null 2>&1; then setsid "$@" >"$log" 2>&1 < /dev/null & else nohup "$@" >"$log" 2>&1 < /dev/null & fi echo $! } _alive() { [ -n "${1:-}" ] && kill -0 "$1" 2>/dev/null; } _server_running() { lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; } _extract_url() { # 优先看脚本自己的日志,兼容回退到手动启动时的 /tmp 日志;没找到不报错 { grep -hEo "https://[a-z0-9-]+\.trycloudflare\.com" "$CF_LOG" /tmp/cf_tunnel.log 2>/dev/null || true; } | tail -1 } start_server() { if _server_running; then echo "✅ server.py 已在跑 (:$PORT)" return fi echo "▶️ 启动 server.py …" ( cd "$PROJ_DIR" && _spawn "$SERVER_LOG" "$PY" server.py "$PORT" ) > "$SERVER_PIDF" # 等监听端口起来 for _ in $(seq 1 20); do _server_running && break sleep 0.5 done if _server_running; then echo "✅ server.py 已启动 (:$PORT) 日志: $SERVER_LOG" else echo "❌ server.py 启动失败,看日志: $SERVER_LOG"; tail -15 "$SERVER_LOG" || true; exit 1 fi } start_tunnel() { echo "▶️ 启动 cloudflared 隧道 (http2) …" : > "$CF_LOG" _spawn "$CF_LOG" cloudflared tunnel --protocol http2 --url "http://localhost:$PORT" > "$CF_PIDF" # 等 URL 出现 local url="" for _ in $(seq 1 40); do url="$(_extract_url)" [ -n "$url" ] && break sleep 0.5 done if [ -n "$url" ]; then # 再等连接注册成功 for _ in $(seq 1 20); do grep -q "Registered tunnel connection" "$CF_LOG" && break sleep 0.5 done echo echo "🌐 公网链接(发给同事即可,无需局域网/改代理):" echo " $url" echo else echo "❌ 隧道未拿到地址,看日志: $CF_LOG"; tail -15 "$CF_LOG" || true; exit 1 fi } stop_all() { echo "⏹ 停止隧道与服务 …" pkill -f "cloudflared tunnel --protocol http2 --url http://localhost:$PORT" 2>/dev/null && echo " - cloudflared 已停" || echo " - 无 cloudflared" # 杀掉占用端口的 server.py local pids; pids="$(lsof -nP -tiTCP:"$PORT" -sTCP:LISTEN 2>/dev/null || true)" if [ -n "$pids" ]; then echo "$pids" | xargs kill 2>/dev/null && echo " - server.py (:$PORT) 已停" else echo " - 无 server.py 监听 :$PORT" fi rm -f "$SERVER_PIDF" "$CF_PIDF" } status() { echo "── 状态 ──────────────────────────────" if _server_running; then echo "server.py : ✅ 运行中 (:$PORT)"; else echo "server.py : ❌ 未运行"; fi if pgrep -f "cloudflared tunnel .*localhost:$PORT" >/dev/null 2>&1; then echo "cloudflared: ✅ 运行中" else echo "cloudflared: ❌ 未运行" fi local url; url="$(_extract_url)" [ -n "$url" ] && echo "公网链接 : $url" || echo "公网链接 : (无)" # 本地健康检查(绕代理) local code; code="$(curl -s -m3 --noproxy '*' -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT/" 2>/dev/null || echo 000)" echo "本地自检 : HTTP $code" echo "─────────────────────────────────────" } case "${1:-start}" in start) start_server; start_tunnel ;; stop) stop_all ;; restart) stop_all; sleep 1; start_server; start_tunnel ;; status) status ;; url) u="$(_extract_url)"; [ -n "$u" ] && echo "$u" || { echo "(隧道未运行)"; exit 1; } ;; *) echo "用法: $0 {start|stop|restart|status|url}"; exit 1 ;; esac