graw.sh
Rama main · réplica de lectura del repositorio original.
#!/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