#!/bin/bash # Unicorn 服务管理脚本 # 优化版本 - 支持完整的服务管理功能,配置完全从 .env 文件读取 set -e # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # 日志函数 log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } is_true() { local value="$1" value=$(echo "$value" | tr '[:upper:]' '[:lower:]') case "$value" in true|1|yes|y|on) return 0 ;; *) return 1 ;; esac } # 加载 .env 文件 load_env() { local env_file=".env" if [ -f "$env_file" ]; then set -a source "$env_file" set +a log_info "已加载 $env_file 文件中的环境变量" else log_warning "$env_file 文件不存在,使用默认值或环境变量" fi } # 从环境变量获取配置,提供默认值 init_config() { # 应用配置 APP_NAME=${APP_NAME:-"app_name"} APP_MODULE=${APP_MODULE:-"app.main:app"} APP_DIR=${APP_DIR:-"$(pwd)"} # 服务器配置 APP_HOST=${APP_HOST:-${HOST:-"0.0.0.0"}} APP_PORT=${APP_PORT:-${PORT:-8000}} APP_WORKERS=${APP_WORKERS:-1} APP_LOG_LEVEL=${APP_LOG_LEVEL:-"info"} DEV_MODE=${DEV_MODE:-"0"} # PID 文件路径 PID_FILE="${APP_DIR}/.${APP_NAME}.pid" # 日志文件路径 LOG_DIR="${APP_DIR}/logs" # 获取当前日期用于日志文件分割 CURRENT_DATE=$(date +%Y-%m-%d) LOG_FILE="${LOG_DIR}/${APP_NAME}-${CURRENT_DATE}.log" log_info "应用名称: $APP_NAME" log_info "应用模块: $APP_MODULE" log_info "应用目录: $APP_DIR" log_info "服务器地址: $APP_HOST" log_info "服务器端口: $APP_PORT" log_info "工作进程数: $APP_WORKERS" log_info "日志级别: $APP_LOG_LEVEL" log_info "开发模式: $([[ $DEV_MODE =~ ^(1|true|yes)$ ]] && echo '已启用' || echo '已禁用')" log_info "日志文件: $LOG_FILE" } # 检查是否在正确的目录 check_directory() { local target_dir="${APP_DIR}" if [ "$(pwd)" != "$target_dir" ]; then log_warning "当前目录不是项目根目录,切换到 $target_dir" cd "$target_dir" || { log_error "无法切换到项目目录: $target_dir" exit 1 } fi } # 检查 uv 是否可用 check_uv() { if ! command -v uv &> /dev/null; then log_error "uv 命令未找到,请确保 uv 已安装" exit 1 fi } # 检查应用模块是否存在 check_app_module() { local module_path=$(echo "$APP_MODULE" | sed 's/:.*//' | sed 's/\./\//g').py if [ ! -f "$module_path" ]; then log_error "应用模块不存在: $module_path" exit 1 fi } # 创建日志目录 setup_logs() { if [ ! -d "$LOG_DIR" ]; then mkdir -p "$LOG_DIR" log_info "创建日志目录: $LOG_DIR" fi } # 启动 Unicorn 服务器 start_server() { local enable_reload=false local reload_from_arg=false # 检查是否传递了 --reload 参数 for arg in "$@"; do if [ "$arg" = "--reload" ]; then enable_reload=true reload_from_arg=true break fi done if [ "$reload_from_arg" = false ]; then if is_true "${APP_DEBUG:-$DEBUG}"; then enable_reload=true fi fi log_info "正在启动 $APP_NAME..." # 检查是否已经在运行 if [ -f "$PID_FILE" ]; then local pid=$(cat "$PID_FILE") if kill -0 "$pid" 2>/dev/null; then log_warning "$APP_NAME 已经在运行 (PID: $pid)" return 0 else log_warning "发现旧的 PID 文件,清理中..." rm -f "$PID_FILE" fi fi # 构建启动命令 local uvicorn_cmd="uv run uvicorn \ $APP_MODULE \ --host $APP_HOST \ --port $APP_PORT \ --workers $APP_WORKERS \ --log-level $APP_LOG_LEVEL \ --access-log" # 根据参数添加 --reload if [ "$enable_reload" = true ]; then uvicorn_cmd="$uvicorn_cmd --reload" log_info "启用热重载模式" else log_info "禁用热重载模式(生产环境推荐)" fi # 启动服务器(使用 >> 追加日志,相同日期日志不会丢失) nohup $uvicorn_cmd >> "$LOG_FILE" 2>&1 & local server_pid=$! echo $server_pid > "$PID_FILE" sleep 2 if kill -0 "$server_pid" 2>/dev/null; then log_success "$APP_NAME 已启动 (PID: $server_pid)" log_info "日志文件: $LOG_FILE" log_info "访问地址: http://$APP_HOST:$APP_PORT" log_info "热重载: $([ "$enable_reload" = true ] && echo "已启用" || echo "已禁用")" else log_error "$APP_NAME 启动失败,请检查日志文件: $LOG_FILE" rm -f "$PID_FILE" exit 1 fi } # 停止 Unicorn 服务器 stop_server() { log_info "正在停止 $APP_NAME..." if [ ! -f "$PID_FILE" ]; then log_warning "PID 文件不存在,$APP_NAME 可能未运行" return 0 fi local pid=$(cat "$PID_FILE") if kill -0 "$pid" 2>/dev/null; then kill -TERM "$pid" sleep 3 if kill -0 "$pid" 2>/dev/null; then log_warning "正常停止失败,尝试强制停止..." kill -KILL "$pid" sleep 1 fi if kill -0 "$pid" 2>/dev/null; then log_error "无法停止进程 $pid" return 1 else rm -f "$PID_FILE" log_success "$APP_NAME 已停止" fi else log_warning "进程 $pid 不存在,清理 PID 文件" rm -f "$PID_FILE" fi } # 重启 Unicorn 服务器 restart_server() { log_info "正在重启 $APP_NAME..." stop_server sleep 2 # 传递所有参数给 start_server(包括可能的 --reload) start_server "$@" } # 查看服务器状态 status_server() { if [ -f "$PID_FILE" ]; then local pid=$(cat "$PID_FILE") if kill -0 "$pid" 2>/dev/null; then log_success "$APP_NAME 正在运行 (PID: $pid)" log_info "端口: $APP_PORT" log_info "工作进程: $APP_WORKERS" return 0 else log_warning "PID 文件存在但进程未运行,清理中..." rm -f "$PID_FILE" fi fi log_info "$APP_NAME 未运行" return 1 } # 查看服务器日志 tail_logs() { local target_log_file="$LOG_FILE" # 检查是否有日期参数 if [ $# -gt 0 ]; then local date_param="$1" # 验证日期格式是否为 YYYY-MM-DD if [[ "$date_param" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then target_log_file="${LOG_DIR}/${APP_NAME}-${date_param}.log" log_info "查看指定日期日志: $date_param" else log_warning "日期参数格式错误,使用默认日期日志" fi fi if [ -f "$target_log_file" ]; then log_info "显示日志文件最后 50 行: $target_log_file" echo "==========================================" tail -50 "$target_log_file" echo "==========================================" # 显示文件基本信息 local file_size=$(du -h "$target_log_file" | cut -f1) local line_count=$(wc -l < "$target_log_file") log_info "文件大小: $file_size, 行数: $line_count" else # 尝试查找最新的日志文件 local latest_log=$(ls -t "${LOG_DIR}/${APP_NAME}-"*.log 2>/dev/null | head -1) if [ -f "$latest_log" ]; then log_warning "指定日志文件不存在,显示最新日志文件: $latest_log" log_info "显示日志文件最后 50 行: $latest_log" echo "==========================================" tail -50 "$latest_log" echo "==========================================" else log_warning "日志文件不存在: $target_log_file" log_info "可用的日志文件:" ls -la "${LOG_DIR}/"*.log 2>/dev/null || echo "无日志文件" fi fi } # 显示帮助信息 show_help() { echo "用法: $0 {start|stop|restart|status|logs|help} [--reload] [日期]" echo "" echo "服务管理命令:" echo " start [--reload] 启动 $APP_NAME 服务器(默认禁用热重载)" echo " stop 停止 $APP_NAME 服务器" echo " restart [--reload] 重启 $APP_NAME 服务器" echo " status 查看 $APP_NAME 服务器状态" echo " logs [YYYY-MM-DD] 查看 $APP_NAME 服务器日志(可指定日期)" echo " help 显示此帮助信息" echo "" echo "可选参数:" echo " --reload 启用热重载模式(开发环境使用)" echo " 日期参数 格式: YYYY-MM-DD,用于查看特定日期日志" echo "" echo "环境变量配置:" echo " 所有配置都从 .env 文件读取,包括:" echo " APP_NAME, APP_MODULE, HOST, PORT, DEBUG, APP_HOST, APP_PORT, APP_WORKERS, APP_LOG_LEVEL 等" echo "" echo "日志功能:" echo " • 日志按日期分割: ${APP_NAME}-YYYY-MM-DD.log" echo " • 相同日期日志自动追加" echo " • 支持查看特定日期日志" echo "" echo "示例:" echo " $0 start # 启动服务器(生产环境推荐)" echo " $0 start --reload # 启动服务器并启用热重载(开发环境)" echo " $0 restart --reload # 重启服务器并启用热重载" echo " $0 status # 查看服务器状态" echo " $0 logs # 查看今天日志" echo " $0 logs 2025-12-14 # 查看指定日期日志" } # 主函数 main() { # 加载环境变量 load_env # 初始化配置 init_config # 执行检查 check_directory check_uv check_app_module setup_logs # 处理命令行参数 case "$1" in start) # 传递剩余参数给 start_server shift start_server "$@" ;; stop) stop_server ;; restart) # 重启时也支持 --reload 参数 shift restart_server "$@" ;; status) status_server ;; logs) # 传递可能的日期参数给 tail_logs shift tail_logs "$@" ;; help|--help|-h) show_help ;; *) if [ -z "$1" ]; then # 默认行为:启动服务器(向后兼容) log_warning "未指定命令,默认执行启动操作" log_warning "建议使用: $0 start" start_server else log_error "未知命令: $1" show_help exit 1 fi ;; esac } # 脚本入口 main "$@"