Files

349 lines
10 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# VPN 专线管理平台 启动脚本 // Logan.Li
# 单二进制内嵌前端交付:构建前端 → 编译 Go → 启动单二进制
# 用法: ./run.sh [build-frontend|build|run|dev|stop|restart|status|help]
#
# 与 opsx-web-lite 的区别:
# - opsx-web-lite 是前后端分离 dev 模式(backend + frontend 独立进程)
# - 本项目把前端构建产物 embed 进 Go 二进制,最终 ./opsx-vpn 单文件运行
set -e
# ============================================================
# 配置区
# ============================================================
PROJECT_NAME="VPN 管理平台"
BACKEND_PORT=8080
FRONTEND_PORT=5173
BINARY_NAME="opsx-vpn"
FRONTEND_DIR="frontend"
BACKEND_DIR="backend"
# 日志配置
MAX_LOG_FILES=3
MAX_LOG_SIZE=$((50 * 1024 * 1024)) # 50MB
# ============================================================
# 通用函数
# ============================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# PID 文件
PID_FILE="$SCRIPT_DIR/.vpn.pid"
# 检查端口是否被占用
check_port() {
lsof -ti:$1 2>/dev/null | head -1
}
# 释放端口
kill_port() {
local pids=$(lsof -ti:$1 2>/dev/null || echo "")
if [ -n "$pids" ]; then
info "释放端口 $1..."
echo "$pids" | xargs kill -9 2>/dev/null || true
sleep 1
fi
}
# 日志轮转(仅启动时调用)
rotate_log() {
local log_file=$1
[ ! -f "$log_file" ] && return 0
local file_size=$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file" 2>/dev/null || echo "0")
if [ "$file_size" -gt "$MAX_LOG_SIZE" ]; then
[ -f "${log_file}.${MAX_LOG_FILES}" ] && rm -f "${log_file}.${MAX_LOG_FILES}"
for i in $(seq $((MAX_LOG_FILES - 1)) -1 1); do
[ -f "${log_file}.${i}" ] && mv "${log_file}.${i}" "${log_file}.$((i + 1))"
done
mv "$log_file" "${log_file}.1"
info "日志已轮转: $log_file"
fi
}
# ============================================================
# 构建前端
# ============================================================
build_frontend() {
info "========== 构建前端 =========="
cd "$SCRIPT_DIR/$FRONTEND_DIR"
if [ ! -d "node_modules" ]; then
warning "node_modules 不存在,正在安装依赖..."
npm install
fi
info "执行 vite build(产物输出到 ../$BACKEND_DIR/dist)..."
npm run build
cd "$SCRIPT_DIR"
success "前端构建完成"
}
# ============================================================
# 编译单二进制
# ============================================================
build_binary() {
info "========== 编译 Go 单二进制 =========="
cd "$SCRIPT_DIR"
# dist 必须存在(embed 需要至少一个文件)
if [ ! -d "$BACKEND_DIR/dist" ]; then
mkdir -p "$BACKEND_DIR/dist"
echo "<!doctype html><html><body>请先构建前端</body></html>" > "$BACKEND_DIR/dist/index.html"
fi
info "go build -ldflags='-s -w' -o $BINARY_NAME ./$BACKEND_DIR"
go build -ldflags="-s -w" -o "$BINARY_NAME" "./$BACKEND_DIR"
success "二进制编译完成: $BINARY_NAME"
}
# ============================================================
# 全量构建(前端 + 后端)
# ============================================================
build_all() {
build_frontend
build_binary
}
# ============================================================
# 运行单二进制
# ============================================================
start_binary() {
info "========== 启动 $PROJECT_NAME =========="
cd "$SCRIPT_DIR"
mkdir -p logs data
rotate_log "logs/vpn.log"
# 每次启动前都将最新前端与后端合并为单一二进制
build_all
kill_port $BACKEND_PORT
export VPN_LISTEN_ADDR=":${BACKEND_PORT}"
nohup ./"$BINARY_NAME" >> logs/vpn.log 2>&1 &
echo $! > "$PID_FILE"
# 等待服务启动
local retries=0
while ! curl -s "http://localhost:$BACKEND_PORT/api/health" > /dev/null 2>&1; do
retries=$((retries + 1))
if [ $retries -ge 30 ]; then
error "服务启动超时,请检查日志: logs/vpn.log"
return 1
fi
sleep 1
done
success "服务已启动 (PID: $(cat "$PID_FILE"), 端口: $BACKEND_PORT)"
info "访问地址: http://localhost:$BACKEND_PORT"
info "默认账号: admin / admin123"
return 0
}
# ============================================================
# 开发模式(前后端分离)
# ============================================================
start_dev() {
info "========== $PROJECT_NAME 开发模式 =========="
cd "$SCRIPT_DIR"
if ! command -v go &> /dev/null; then
error "Go 未安装,请先安装 Go"
exit 1
fi
if ! command -v npm &> /dev/null; then
error "npm 未安装,请先安装 Node.js"
exit 1
fi
mkdir -p logs data
# 后端:go run(前台 / 后台均可,这里后台以便同时跑前端)
kill_port $BACKEND_PORT
export VPN_LISTEN_ADDR=":${BACKEND_PORT}"
export GIN_MODE=debug
nohup go run "./$BACKEND_DIR" >> logs/vpn-dev.log 2>&1 &
echo $! > "$PID_FILE.dev"
info "后端已启动 (go run, 端口: $BACKEND_PORT)"
# 前端:vite dev server
cd "$FRONTEND_DIR"
if [ ! -d "node_modules" ]; then
warning "node_modules 不存在,正在安装依赖..."
npm install
fi
kill_port $FRONTEND_PORT
nohup npm run dev >> ../logs/frontend-dev.log 2>&1 &
echo $! > "../.frontend.pid"
cd "$SCRIPT_DIR"
# 等待前端就绪
local retries=0
while ! curl -s "http://localhost:$FRONTEND_PORT" > /dev/null 2>&1; do
retries=$((retries + 1))
if [ $retries -ge 40 ]; then
error "前端启动超时,请检查日志: logs/frontend-dev.log"
return 1
fi
sleep 1
done
success "开发模式已启动"
info "前端地址: http://localhost:$FRONTEND_PORT(代理 /api 到 $BACKEND_PORT)"
info "后端地址: http://localhost:$BACKEND_PORT"
}
# ============================================================
# 停止服务
# ============================================================
stop_all() {
info "正在停止服务..."
# 单二进制
if [ -f "$PID_FILE" ]; then
local pid=$(cat "$PID_FILE" 2>/dev/null || echo "")
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
success "单二进制服务已停止"
fi
rm -f "$PID_FILE"
fi
kill_port $BACKEND_PORT
# 开发模式后端
if [ -f "$PID_FILE.dev" ]; then
local devpid=$(cat "$PID_FILE.dev" 2>/dev/null || echo "")
if [ -n "$devpid" ] && kill -0 "$devpid" 2>/dev/null; then
kill "$devpid" 2>/dev/null || true
success "后端 dev 进程已停止"
fi
rm -f "$PID_FILE.dev"
fi
# 开发模式前端
if [ -f "$SCRIPT_DIR/.frontend.pid" ]; then
local fpid=$(cat "$SCRIPT_DIR/.frontend.pid" 2>/dev/null || echo "")
if [ -n "$fpid" ] && kill -0 "$fpid" 2>/dev/null; then
kill "$fpid" 2>/dev/null || true
success "前端 dev 进程已停止"
fi
rm -f "$SCRIPT_DIR/.frontend.pid"
fi
kill_port $FRONTEND_PORT
info "所有服务已停止"
}
# ============================================================
# 状态查看
# ============================================================
show_status() {
info "服务状态:"
if [ -n "$(check_port $BACKEND_PORT)" ]; then
local pid=$(check_port $BACKEND_PORT)
success "后端服务: 运行中 (PID: $pid, 端口: $BACKEND_PORT)"
else
warning "后端服务: 未运行"
fi
if [ -n "$(check_port $FRONTEND_PORT)" ]; then
local fpid=$(lsof -ti:$FRONTEND_PORT 2>/dev/null | grep -v Chrome | head -1)
if [ -n "$fpid" ]; then
success "前端 dev : 运行中 (PID: $fpid, 端口: $FRONTEND_PORT)"
fi
else
info "前端 dev : 未运行(生产模式由后端内嵌提供)"
fi
}
# ============================================================
# 帮助
# ============================================================
show_help() {
echo "$PROJECT_NAME 启动脚本"
echo ""
echo "用法: ./run.sh [命令]"
echo ""
echo "命令:"
echo " run 构建并后台运行单二进制(默认)"
echo " build 全量构建(前端 + 后端二进制)"
echo " build-frontend 仅构建前端(输出到 backend/dist)"
echo " build-binary 仅编译 Go 二进制"
echo " dev 开发模式(前后端分离,vite 代理到后端)"
echo " stop 停止所有服务"
echo " restart 重启服务"
echo " status 查看服务状态"
echo " help 显示帮助信息"
echo ""
echo "环境变量:"
echo " VPN_LISTEN_ADDR 监听地址(默认 :8080)"
echo " VPN_DATA_DIR 数据目录(默认 ./data)"
echo ""
echo "示例:"
echo " ./run.sh # 构建并运行"
echo " ./run.sh build # 仅构建"
echo " ./run.sh dev # 开发模式"
echo " ./run.sh stop # 停止服务"
}
# ============================================================
# 主入口
# ============================================================
main() {
local command=${1:-run}
case "$command" in
run)
start_binary
;;
build)
build_all
;;
build-frontend)
build_frontend
;;
build-binary)
build_binary
;;
dev)
start_dev
;;
stop)
stop_all
;;
restart)
stop_all
sleep 2
start_binary
;;
status)
show_status
;;
help|--help|-h)
show_help
;;
*)
error "未知命令: $command"
show_help
exit 1
;;
esac
}
main "$@"