Ctrl K
ComunidadGuías…

graw.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# =============================================================================
# graw.sh — Graw 非 Docker「本地构建 + 守护进程」一键命令(Linux / macOS)
#
# 与开发用的 start.sh(前后端分开热启动)不同,本脚本走「生产形态」:
#   构建前端 dist -> 编译 Go 后端 -> 由后端静态托管 -> 守护进程后台运行。
#
# 用法:
#   ./graw.sh deps       安装依赖(Go module + npm install)
#   ./graw.sh build      构建前端(frontend/dist)与 Go 后端二进制
#   ./graw.sh start      依赖/构建缺失时自动补齐,然后启动守护进程
#   ./graw.sh stop       停止守护进程
#   ./graw.sh restart    重启
#   ./graw.sh status     运行状态(进程 + 健康检查)
#   ./graw.sh log        跟踪运行日志(Ctrl+C 退出,不影响服务)
#
# 环境变量:
#   GRAW_PORT=8000       面板监听端口
#   GRAW_HOST=0.0.0.0    监听地址(仅本机可改 127.0.0.1)
#   FORCE_BUILD=1        start 时强制重新构建前端
# =============================================================================
set -e

ROOT="$(cd "$(dirname "$0")" && pwd)"
BACKEND_GO_DIR="$ROOT/backend-go"
FRONTEND_DIR="$ROOT/frontend"
BIN="$BACKEND_GO_DIR/graw-go"
LOG_DIR="$ROOT/logs"
LOG_FILE="$LOG_DIR/graw.log"
PID_FILE="$LOG_DIR/graw.pid"
HOST="${GRAW_HOST:-0.0.0.0}"
PORT="${GRAW_PORT:-8000}"
ADDR="$HOST:$PORT"

log()  { echo "[graw] $*"; }
fail() { echo "[graw] 错误: $*" >&2; exit 1; }

# 安装依赖:Go module 幂等下载;前端 node_modules 缺失时安装
do_deps() {
  command -v go >/dev/null 2>&1 || fail "未找到 go 命令,请先安装 Go 1.23+"
  command -v npm >/dev/null 2>&1 || fail "未找到 npm 命令,请先安装 Node.js 16+"
  log "下载 Go 模块(已缓存会自动跳过)..."
  (cd "$BACKEND_GO_DIR" && go mod download)
  if [ ! -d "$FRONTEND_DIR/node_modules" ]; then
    log "安装前端依赖(npm install,首次较慢)..."
    (cd "$FRONTEND_DIR" && npm install --no-audit --no-fund)
  fi
}

# 构建前端:产物在 frontend/dist,由 Go 后端同源托管
do_build_frontend() {
  [ -d "$FRONTEND_DIR/node_modules" ] || do_deps
  log "构建前端(npm run build)..."
  (cd "$FRONTEND_DIR" && npm run build)
}

# 编译 Go 后端:纯静态二进制,输出到 backend-go/graw-go
do_build_backend() {
  command -v go >/dev/null 2>&1 || fail "未找到 go 命令,请先安装 Go 1.23+"
  log "编译 Go 后端(CGO_ENABLED=0)..."
  (cd "$BACKEND_GO_DIR" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o "$BIN" .)
}

do_build() {
  do_build_frontend
  do_build_backend
}

# 守护进程是否在运行(PID 文件 + 进程存活双确认)
is_running() {
  [ -f "$PID_FILE" ] || return 1
  local pid
  pid="$(cat "$PID_FILE" 2>/dev/null || true)"
  [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
}

# 启动守护进程:缺依赖/缺构建时自动补齐,nohup 后台运行并写 PID 文件
do_start() {
  if is_running; then
    log "已在运行(PID $(cat "$PID_FILE")),如需重启请执行 ./graw.sh restart"
    exit 0
  fi
  do_deps
  if [ ! -f "$FRONTEND_DIR/dist/index.html" ] || [ -n "$FORCE_BUILD" ]; then
    do_build_frontend
  fi
  do_build_backend
  mkdir -p "$LOG_DIR"
  log "启动守护进程:graw-go 监听 $ADDR ..."
  (
    cd "$BACKEND_GO_DIR"
    GRAW_ADDR="$ADDR" \
    GRAW_DATA_DIR="$ROOT/backend/data" \
    GRAW_FRONTEND_DIST="$FRONTEND_DIR/dist" \
      nohup "$BIN" >> "$LOG_FILE" 2>&1 &
    echo $! > "$PID_FILE"
  )
  wait_healthy
}

# 健康检查:最多等 30 秒,期间轮询 /api/health
wait_healthy() {
  for _ in $(seq 1 30); do
    if health_ok; then
      log "启动成功 ✔  访问: http://localhost:$PORT  (日志: $LOG_FILE,PID: $(cat "$PID_FILE"))"
      return 0
    fi
    if ! is_running; then
      fail "进程已退出,请查看日志:tail -n 50 $LOG_FILE"
    fi
    sleep 1
  done
  log "警告: 服务未在 30 秒内通过健康检查(可能仍在启动),请查看日志确认"
}

# 健康探测:优先 curl,其次 wget,最后回退 TCP 端口连通性
health_ok() {
  if command -v curl >/dev/null 2>&1; then
    curl -fsS -o /dev/null --max-time 3 "http://127.0.0.1:$PORT/api/health" 2>/dev/null
  elif command -v wget >/dev/null 2>&1; then
    wget -q -O /dev/null -T 3 "http://127.0.0.1:$PORT/api/health" 2>/dev/null
  else
    (exec 3<>"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null
  fi
}

# 停止守护进程:SIGTERM 优雅退出,5 秒后 SIGKILL 兜底
do_stop() {
  if ! is_running; then
    log "未在运行"
    rm -f "$PID_FILE"
    return 0
  fi
  local pid
  pid="$(cat "$PID_FILE")"
  log "停止守护进程(PID $pid)..."
  kill "$pid" 2>/dev/null || true
  for _ in $(seq 1 10); do
    kill -0 "$pid" 2>/dev/null || break
    sleep 0.5
  done
  if kill -0 "$pid" 2>/dev/null; then
    log "强制终止..."
    kill -9 "$pid" 2>/dev/null || true
  fi
  rm -f "$PID_FILE"
  log "已停止"
}

do_status() {
  if is_running; then
    log "运行中(PID $(cat "$PID_FILE"),端口 $PORT)"
    if health_ok; then log "健康检查: 正常 ✔"; else log "健康检查: 未就绪(可能正在启动或异常,见 $LOG_FILE)"; fi
  else
    log "未在运行"
    exit 1
  fi
}

case "${1:-}" in
  deps)    do_deps ;;
  build)   do_build ;;
  start)   do_start ;;
  stop)    do_stop ;;
  restart) do_stop; do_start ;;
  status)  do_status ;;
  log)     [ -f "$LOG_FILE" ] || fail "暂无日志: $LOG_FILE"; tail -n 100 -f "$LOG_FILE" ;;
  *)       sed -n '2,21p' "$0" | sed 's/^# \{0,1\}//'; exit 1 ;;
esac