590 lines
15 KiB
Bash
Executable File
590 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
# ============================================
|
||
# 陇耘鲜达 Backend 测试环境启动脚本
|
||
# 适配: python3.9+(推荐 venv;显式不使用 python2)
|
||
#
|
||
# 测试服务器目录示例: /root/zkxw/backend
|
||
# 默认监听: 127.0.0.1:10002(裸机 Nginx 反代;勿与生产 8111 冲突)
|
||
# 旧双机拓扑 0.0.0.0:8112 已废弃;手动 start/restart 时也走本默认,避免 Nginx 502。
|
||
#
|
||
# 首次部署:
|
||
# cd /root/zkxw/backend
|
||
# cp .env.test.example .env.test # 或从 .env.example 复制后按测试库填写
|
||
# python3.9 -m venv .venv && ./.venv/bin/pip install -r requirements.txt
|
||
# ./start-test.sh activate # source .venv/bin/activate
|
||
# ./start-test.sh start
|
||
# ============================================
|
||
|
||
set -euo pipefail
|
||
|
||
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
cd "$BASE_DIR"
|
||
|
||
APP_MODULE="${APP_MODULE:-app.main:app}"
|
||
HOST="${HOST:-127.0.0.1}"
|
||
PORT="${PORT:-10002}"
|
||
WORKERS="${WORKERS:-1}"
|
||
LOG_LEVEL="${LOG_LEVEL:-debug}"
|
||
ENV_FILE="${ENV_FILE:-$BASE_DIR/.env.test}"
|
||
|
||
VENV_DIR="${VENV_DIR:-$BASE_DIR/.venv}"
|
||
ACTIVATE_SCRIPT="$VENV_DIR/bin/activate"
|
||
PYTHON_BIN="${PYTHON_BIN:-$VENV_DIR/bin/python}"
|
||
MIN_PYTHON_MAJOR=3
|
||
MIN_PYTHON_MINOR=9
|
||
SYSTEM_PYTHON=""
|
||
|
||
LOG_DIR="$BASE_DIR/logs"
|
||
LOG_FILE="$LOG_DIR/backend-test.log"
|
||
PID_FILE="$BASE_DIR/backend-test.pid"
|
||
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
NC='\033[0m'
|
||
|
||
log_info() {
|
||
echo -e "${GREEN}[INFO]${NC} $1"
|
||
}
|
||
|
||
log_warn() {
|
||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}[ERROR]${NC} $1"
|
||
}
|
||
|
||
show_help() {
|
||
cat << 'EOF'
|
||
用法:
|
||
./start-test.sh [start|stop|restart|status|logs|activate]
|
||
|
||
命令:
|
||
start 启动服务(默认)
|
||
stop 停止服务
|
||
restart 重启服务
|
||
status 查看状态
|
||
logs 查看日志(最近100行)
|
||
activate 激活 .venv 并进入交互 shell(等价 source .venv/bin/activate)
|
||
|
||
可选环境变量:
|
||
HOST=127.0.0.1
|
||
PORT=10002
|
||
WORKERS=1
|
||
LOG_LEVEL=debug
|
||
APP_MODULE=app.main:app
|
||
VENV_DIR=./.venv
|
||
PYTHON_FOR_VENV=python3.9
|
||
ENV_FILE=./.env.test
|
||
|
||
测试部署目录(示例):
|
||
/root/zkxw/backend
|
||
|
||
cd /root/zkxw/backend && ./start-test.sh start
|
||
|
||
说明:
|
||
- 读取 .env.test(非 .env.production)
|
||
- 默认 127.0.0.1:10002(Nginx 反代);生产 start-prod.sh 使用 8111
|
||
- 可与生产共用 venv,但 PID/日志文件独立,勿混用 start/stop 命令
|
||
- stop/restart 除 PID 文件外,还会按端口清理本应用 uvicorn,避免僵尸进程占端口导致发版假成功
|
||
EOF
|
||
}
|
||
|
||
ensure_dirs() {
|
||
mkdir -p "$LOG_DIR"
|
||
}
|
||
|
||
python_version_ge() {
|
||
local ver="$1"
|
||
local major="${ver%%.*}"
|
||
local minor="${ver#*.}"
|
||
if [ "$major" -gt "$MIN_PYTHON_MAJOR" ]; then
|
||
return 0
|
||
fi
|
||
if [ "$major" -eq "$MIN_PYTHON_MAJOR" ] && [ "$minor" -ge "$MIN_PYTHON_MINOR" ]; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
resolve_system_python() {
|
||
local candidate ver
|
||
|
||
if [ -n "${PYTHON_FOR_VENV:-}" ] && command -v "$PYTHON_FOR_VENV" >/dev/null 2>&1; then
|
||
ver="$("$PYTHON_FOR_VENV" -c 'import sys; print("{}.{}".format(sys.version_info[0], sys.version_info[1]))')"
|
||
if python_version_ge "$ver"; then
|
||
echo "$PYTHON_FOR_VENV"
|
||
return 0
|
||
fi
|
||
log_error "PYTHON_FOR_VENV=$PYTHON_FOR_VENV 版本为 $ver,需要 >= ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR}"
|
||
exit 1
|
||
fi
|
||
|
||
for candidate in python3.11 python3.10 python3.9 python3; do
|
||
if command -v "$candidate" >/dev/null 2>&1; then
|
||
ver="$("$candidate" -c 'import sys; print("{}.{}".format(sys.version_info[0], sys.version_info[1]))')"
|
||
if python_version_ge "$ver"; then
|
||
echo "$candidate"
|
||
return 0
|
||
fi
|
||
fi
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
check_python() {
|
||
if ! SYSTEM_PYTHON="$(resolve_system_python)"; then
|
||
log_error "未找到 python${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR}+,请安装 python39 或设置 PYTHON_FOR_VENV=python3.9"
|
||
exit 1
|
||
fi
|
||
|
||
log_info "系统 Python: $SYSTEM_PYTHON ($("$SYSTEM_PYTHON" --version 2>&1))"
|
||
}
|
||
|
||
ensure_venv_python_version() {
|
||
local ver
|
||
ver="$("$PYTHON_BIN" -c 'import sys; print("{}.{}".format(sys.version_info[0], sys.version_info[1]))')"
|
||
if python_version_ge "$ver"; then
|
||
log_info "虚拟环境 Python: $ver ($VENV_DIR)"
|
||
return 0
|
||
fi
|
||
|
||
log_error "虚拟环境 Python 版本过低: $ver(需要 >= ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR})"
|
||
log_error "当前虚拟环境目录: $VENV_DIR"
|
||
log_warn "请执行: rm -rf \"$VENV_DIR\" && $SYSTEM_PYTHON -m venv \"$VENV_DIR\" && \"$VENV_DIR/bin/pip\" install -r requirements.txt"
|
||
exit 1
|
||
}
|
||
|
||
has_uvicorn() {
|
||
"$PYTHON_BIN" -c "import uvicorn" >/dev/null 2>&1
|
||
}
|
||
|
||
ensure_venv_and_runtime() {
|
||
log_info "虚拟环境目录: $VENV_DIR"
|
||
|
||
if [ ! -d "$VENV_DIR" ]; then
|
||
log_warn "未找到虚拟环境,开始自动创建: $VENV_DIR"
|
||
if ! "$SYSTEM_PYTHON" -m venv "$VENV_DIR" >/dev/null 2>&1; then
|
||
log_error "自动创建 venv 失败,请先安装 python3-venv 或手动创建虚拟环境"
|
||
log_warn "可尝试: $SYSTEM_PYTHON -m venv \"$VENV_DIR\""
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
if [ ! -x "$PYTHON_BIN" ]; then
|
||
log_error "虚拟环境 python 不存在: $PYTHON_BIN"
|
||
exit 1
|
||
fi
|
||
|
||
ensure_venv_python_version
|
||
|
||
if [ ! -x "$VENV_DIR/bin/pip" ]; then
|
||
log_warn "虚拟环境 pip 不存在,尝试引导安装"
|
||
"$PYTHON_BIN" -m ensurepip --upgrade >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
if ! has_uvicorn; then
|
||
log_warn "未检测到 uvicorn,开始安装依赖 requirements.txt"
|
||
if [ ! -x "$VENV_DIR/bin/pip" ]; then
|
||
log_error "pip 仍不可用,请手动执行依赖初始化"
|
||
exit 1
|
||
fi
|
||
"$VENV_DIR/bin/pip" install --upgrade pip
|
||
"$VENV_DIR/bin/pip" install -r "$BASE_DIR/requirements.txt"
|
||
if ! has_uvicorn; then
|
||
log_error "依赖安装后仍无法 import uvicorn,请检查: $PYTHON_BIN -m pip install -r requirements.txt"
|
||
exit 1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# 读取 PID 文件中的进程号;必须存活且确认为本应用 uvicorn(防止脏 PID/PID=1 误杀)
|
||
read_pid_file() {
|
||
if [ ! -f "$PID_FILE" ]; then
|
||
return 0
|
||
fi
|
||
local pid
|
||
pid="$(tr -d '[:space:]' < "$PID_FILE" 2>/dev/null || true)"
|
||
if [ -z "$pid" ] || ! [[ "$pid" =~ ^[0-9]+$ ]]; then
|
||
return 0
|
||
fi
|
||
# 拒绝系统关键 PID 与明显非法值
|
||
if [ "$pid" -le 1 ]; then
|
||
return 0
|
||
fi
|
||
if ! ps -p "$pid" >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
if ! is_our_uvicorn_pid "$pid"; then
|
||
return 0
|
||
fi
|
||
echo "$pid"
|
||
}
|
||
|
||
# 进程命令行是否像本目录下的测试 uvicorn
|
||
is_our_uvicorn_pid() {
|
||
local pid="$1"
|
||
local cmd=""
|
||
if [ ! -r "/proc/$pid/cmdline" ]; then
|
||
return 1
|
||
fi
|
||
cmd="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)"
|
||
case "$cmd" in
|
||
*"${BASE_DIR}"*"uvicorn"*"${APP_MODULE}"*) return 0 ;;
|
||
*"${VENV_DIR}/bin/python"*"uvicorn"*"${APP_MODULE}"*"--port ${PORT}"*) return 0 ;;
|
||
*"${VENV_DIR}/bin/python"*"uvicorn"*"${APP_MODULE}"*"--port=${PORT}"*) return 0 ;;
|
||
esac
|
||
# 兼容无绝对路径、仅按 port + app 模块匹配(仍限制在本机测试端口)
|
||
case "$cmd" in
|
||
*"uvicorn"*"${APP_MODULE}"*"--port ${PORT}"*|*"uvicorn"*"${APP_MODULE}"*"--port=${PORT}"*)
|
||
return 0
|
||
;;
|
||
esac
|
||
return 1
|
||
}
|
||
|
||
# 列出占用 PORT 的监听进程 PID
|
||
list_listen_pids_on_port() {
|
||
local pids=""
|
||
if command -v ss >/dev/null 2>&1; then
|
||
pids="$(ss -lptn "sport = :${PORT}" 2>/dev/null | sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' | sort -u)"
|
||
fi
|
||
if [ -z "$pids" ] && command -v lsof >/dev/null 2>&1; then
|
||
pids="$(lsof -tiTCP:"${PORT}" -sTCP:LISTEN 2>/dev/null | sort -u || true)"
|
||
fi
|
||
if [ -z "$pids" ] && command -v fuser >/dev/null 2>&1; then
|
||
pids="$(fuser "${PORT}/tcp" 2>/dev/null | tr -s '[:space:]' '\n' | grep -E '^[0-9]+$' | sort -u || true)"
|
||
fi
|
||
echo "$pids"
|
||
}
|
||
|
||
# 收集应停止的 PID:PID 文件 + 占用本端口的本应用进程
|
||
collect_managed_pids() {
|
||
local pid p seen=""
|
||
pid="$(read_pid_file || true)"
|
||
if [ -n "${pid:-}" ]; then
|
||
echo "$pid"
|
||
seen=" $pid "
|
||
fi
|
||
for p in $(list_listen_pids_on_port); do
|
||
if [ -z "$p" ]; then
|
||
continue
|
||
fi
|
||
case "$seen" in
|
||
*" $p "*) continue ;;
|
||
esac
|
||
if is_our_uvicorn_pid "$p"; then
|
||
echo "$p"
|
||
seen="${seen}${p} "
|
||
fi
|
||
done
|
||
}
|
||
|
||
port_is_in_use() {
|
||
local p
|
||
p="$(list_listen_pids_on_port | head -n 1)"
|
||
[ -n "$p" ]
|
||
}
|
||
|
||
wait_port_free() {
|
||
local i
|
||
for i in $(seq 1 20); do
|
||
if ! port_is_in_use; then
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
return 1
|
||
}
|
||
|
||
wait_health_ok() {
|
||
local i url="http://127.0.0.1:${PORT}/health"
|
||
for i in $(seq 1 30); do
|
||
if curl -fsS --connect-timeout 1 --max-time 2 "$url" >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
return 1
|
||
}
|
||
|
||
is_running() {
|
||
local pid
|
||
pid="$(read_pid_file || true)"
|
||
if [ -n "${pid:-}" ]; then
|
||
return 0
|
||
fi
|
||
# PID 文件失效时,仍以「本端口上的本应用进程」为准,避免误判已停止
|
||
for pid in $(list_listen_pids_on_port); do
|
||
if is_our_uvicorn_pid "$pid"; then
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# 仅扩展「已确认属于本应用」的进程树;禁止对 PID<=1 或未校验进程做树展开
|
||
expand_our_pid_tree() {
|
||
local pid="$1"
|
||
local c
|
||
if [ -z "$pid" ] || [ "$pid" -le 1 ]; then
|
||
return 0
|
||
fi
|
||
if ! ps -p "$pid" >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
if ! is_our_uvicorn_pid "$pid"; then
|
||
return 0
|
||
fi
|
||
echo "$pid"
|
||
for c in $(pgrep -P "$pid" 2>/dev/null || true); do
|
||
# 子进程可能是纯 python worker,命令行仍含 uvicorn/app.main;否则跳过
|
||
if [ -n "$c" ] && [ "$c" -gt 1 ] && is_our_uvicorn_pid "$c"; then
|
||
expand_our_pid_tree "$c"
|
||
elif [ -n "$c" ] && [ "$c" -gt 1 ] && ps -p "$c" >/dev/null 2>&1; then
|
||
# worker 偶发 cmdline 不含完整模块名时:仅当父进程已是本应用才收其子进程
|
||
echo "$c"
|
||
fi
|
||
done
|
||
}
|
||
|
||
kill_pids_graceful_then_force() {
|
||
local pid
|
||
local any=0
|
||
local all_pids=""
|
||
local p
|
||
for pid in "$@"; do
|
||
if [ -z "$pid" ] || [ "$pid" -le 1 ]; then
|
||
continue
|
||
fi
|
||
for p in $(expand_our_pid_tree "$pid"); do
|
||
if [ -n "$p" ] && [ "$p" -gt 1 ]; then
|
||
all_pids="${all_pids}${p} "
|
||
fi
|
||
done
|
||
done
|
||
# shellcheck disable=SC2086
|
||
set -- $all_pids
|
||
|
||
for pid in "$@"; do
|
||
if [ -z "$pid" ] || [ "$pid" -le 1 ]; then
|
||
continue
|
||
fi
|
||
if ! ps -p "$pid" >/dev/null 2>&1; then
|
||
continue
|
||
fi
|
||
any=1
|
||
log_info "正在停止测试服务 (PID: $pid)"
|
||
kill -TERM "$pid" >/dev/null 2>&1 || true
|
||
done
|
||
if [ "$any" -eq 0 ]; then
|
||
return 0
|
||
fi
|
||
|
||
local i left
|
||
for i in $(seq 1 10); do
|
||
left=0
|
||
for pid in "$@"; do
|
||
if [ -n "$pid" ] && [ "$pid" -gt 1 ] && ps -p "$pid" >/dev/null 2>&1; then
|
||
left=1
|
||
break
|
||
fi
|
||
done
|
||
if [ "$left" -eq 0 ]; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
for pid in "$@"; do
|
||
if [ -n "$pid" ] && [ "$pid" -gt 1 ] && ps -p "$pid" >/dev/null 2>&1; then
|
||
log_warn "优雅停止超时,执行强制停止 (PID: $pid)"
|
||
kill -9 "$pid" >/dev/null 2>&1 || true
|
||
fi
|
||
done
|
||
}
|
||
|
||
# 停止本应用在测试端口上的全部实例(含 PID 文件失效的僵尸进程)
|
||
stop_server() {
|
||
local pids
|
||
pids="$(collect_managed_pids | sort -u | tr '\n' ' ')"
|
||
# shellcheck disable=SC2086
|
||
set -- $pids
|
||
|
||
if [ "$#" -eq 0 ]; then
|
||
# 端口仍被非本应用占用时给出明确提示
|
||
if port_is_in_use; then
|
||
log_warn "测试服务未由本脚本托管,但端口 ${PORT} 仍被占用: $(list_listen_pids_on_port | tr '\n' ' ')"
|
||
log_warn "请人工确认后再启动,避免绑端口失败"
|
||
else
|
||
log_warn "测试服务未运行"
|
||
fi
|
||
rm -f "$PID_FILE"
|
||
return 0
|
||
fi
|
||
|
||
kill_pids_graceful_then_force "$@"
|
||
|
||
# 若仍有本应用占端口,再扫一遍强杀
|
||
pids="$(collect_managed_pids | sort -u | tr '\n' ' ')"
|
||
# shellcheck disable=SC2086
|
||
set -- $pids
|
||
if [ "$#" -gt 0 ]; then
|
||
log_warn "仍检测到本应用进程,执行二次强制停止"
|
||
for pid in "$@"; do
|
||
kill -9 "$pid" >/dev/null 2>&1 || true
|
||
done
|
||
fi
|
||
|
||
rm -f "$PID_FILE"
|
||
|
||
if ! wait_port_free; then
|
||
log_error "端口 ${PORT} 仍被占用,无法确认已停止: $(list_listen_pids_on_port | tr '\n' ' ')"
|
||
return 1
|
||
fi
|
||
|
||
log_info "测试服务已停止"
|
||
return 0
|
||
}
|
||
|
||
start_server() {
|
||
check_python
|
||
ensure_venv_and_runtime
|
||
ensure_dirs
|
||
|
||
# 已有本应用在跑(PID 文件或端口探测)则直接返回
|
||
if is_running; then
|
||
local running_pid
|
||
running_pid="$(read_pid_file || true)"
|
||
if [ -z "${running_pid:-}" ]; then
|
||
running_pid="$(collect_managed_pids | head -n 1)"
|
||
fi
|
||
log_warn "测试服务已在运行 (PID: ${running_pid:-unknown})"
|
||
# 回写 PID 文件,避免后续 stop 找不到
|
||
if [ -n "${running_pid:-}" ]; then
|
||
echo "$running_pid" > "$PID_FILE"
|
||
fi
|
||
exit 0
|
||
fi
|
||
|
||
# 端口被占用但不是本应用:拒绝启动,避免静默失败后健康检查打到别人的服务
|
||
if port_is_in_use; then
|
||
log_error "端口 ${PORT} 已被占用且无法识别为本测试服务: $(list_listen_pids_on_port | tr '\n' ' ')"
|
||
log_error "请先释放端口后再 start / restart"
|
||
exit 1
|
||
fi
|
||
|
||
rm -f "$PID_FILE"
|
||
|
||
log_info "启动测试环境服务..."
|
||
log_info "监听地址: $HOST:$PORT"
|
||
log_info "工作进程: $WORKERS"
|
||
log_info "日志文件: $LOG_FILE"
|
||
log_info "环境文件: $ENV_FILE"
|
||
|
||
if [ ! -f "$ENV_FILE" ]; then
|
||
log_error "环境文件不存在: $ENV_FILE"
|
||
log_warn "可先执行: cp .env.test.example .env.test 并按测试库修改"
|
||
exit 1
|
||
fi
|
||
|
||
export ENV_FILE
|
||
nohup "$PYTHON_BIN" -m uvicorn "$APP_MODULE" \
|
||
--host "$HOST" \
|
||
--port "$PORT" \
|
||
--workers "$WORKERS" \
|
||
--log-level "$LOG_LEVEL" \
|
||
>> "$LOG_FILE" 2>&1 &
|
||
|
||
echo $! > "$PID_FILE"
|
||
|
||
if ! wait_health_ok; then
|
||
log_error "测试服务启动失败(健康检查超时),请检查日志: $LOG_FILE"
|
||
# 启动失败时尽量清掉半拉子进程,避免留下占端口僵尸
|
||
stop_server || true
|
||
exit 1
|
||
fi
|
||
|
||
# 优先把 PID 文件写成实际监听端口的进程,避免 nohup 壳/worker 不一致
|
||
local listen_pid
|
||
listen_pid="$(list_listen_pids_on_port | head -n 1 || true)"
|
||
if [ -n "${listen_pid:-}" ]; then
|
||
echo "$listen_pid" > "$PID_FILE"
|
||
fi
|
||
|
||
log_info "测试服务启动成功 (PID: $(cat "$PID_FILE"))"
|
||
log_info "健康检查: http://127.0.0.1:$PORT/health"
|
||
log_info "API 文档: http://127.0.0.1:$PORT/docs"
|
||
}
|
||
|
||
status_server() {
|
||
local pid
|
||
if is_running; then
|
||
pid="$(read_pid_file || true)"
|
||
if [ -z "${pid:-}" ]; then
|
||
pid="$(collect_managed_pids | head -n 1)"
|
||
fi
|
||
echo -e "${GREEN}RUNNING (test)${NC} PID=${pid:-unknown} HOST=$HOST PORT=$PORT"
|
||
else
|
||
if port_is_in_use; then
|
||
echo -e "${YELLOW}PORT BUSY (test)${NC} PORT=$PORT pids=$(list_listen_pids_on_port | tr '\n' ' ')"
|
||
else
|
||
echo -e "${RED}STOPPED (test)${NC}"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
show_logs() {
|
||
ensure_dirs
|
||
if [ ! -f "$LOG_FILE" ]; then
|
||
log_warn "暂无日志文件: $LOG_FILE"
|
||
exit 0
|
||
fi
|
||
tail -n 100 "$LOG_FILE"
|
||
}
|
||
|
||
activate_venv() {
|
||
if [ ! -f "$ACTIVATE_SCRIPT" ]; then
|
||
log_error "虚拟环境激活脚本不存在: $ACTIVATE_SCRIPT"
|
||
log_warn "可先执行: python3.9 -m venv .venv && ./.venv/bin/pip install -r requirements.txt"
|
||
exit 1
|
||
fi
|
||
|
||
log_info "激活虚拟环境: source $ACTIVATE_SCRIPT"
|
||
exec "$SHELL" -i -c "cd '$BASE_DIR' && source '$ACTIVATE_SCRIPT' && exec -i '$SHELL'"
|
||
}
|
||
|
||
ACTION="${1:-start}"
|
||
case "$ACTION" in
|
||
start)
|
||
start_server
|
||
;;
|
||
stop)
|
||
stop_server
|
||
;;
|
||
restart)
|
||
stop_server || true
|
||
start_server
|
||
;;
|
||
status)
|
||
status_server
|
||
;;
|
||
logs)
|
||
show_logs
|
||
;;
|
||
activate)
|
||
activate_venv
|
||
;;
|
||
-h|--help|help)
|
||
show_help
|
||
;;
|
||
*)
|
||
log_error "未知命令: $ACTION"
|
||
show_help
|
||
exit 1
|
||
;;
|
||
esac
|