145 lines
5.4 KiB
Bash
Executable File
145 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# scense 应用一键部署脚本(元景项目)
|
||
# 唯一部署单元:一个入口 app/scense.py + 一个端口 ${SERVER_PORT:-9380}
|
||
# 门禁:部署完成后 30s 端口监听门禁——30s 内未监听自动重启一次兜底,
|
||
# 再等 30s 仍失败才输出诊断并以非零退出(不静默通过)。
|
||
# =============================================================================
|
||
set -uo pipefail
|
||
|
||
APP_NAME="scense"
|
||
APP_PORT="${SERVER_PORT:-9380}"
|
||
WORKDIR="$(cd "$(dirname "$0")" && pwd)"
|
||
VENV_DIR="$WORKDIR/venv"
|
||
PKGS_DIR="$WORKDIR/pkgs"
|
||
LOGS_DIR="$WORKDIR/logs"
|
||
ENV_FILE="$WORKDIR/.env"
|
||
|
||
# 模块清单(基础引用 + 本应用业务模块,与 yuanjing_spec.json generated_modules 一致)
|
||
FOUNDATION_MODULES=(apppublic sqlor ahserver bricks_for_python appbase rbac)
|
||
BIZ_MODULES=(world scene entity script_engine world_snapshot world_sync)
|
||
ALL_MODULES=("${FOUNDATION_MODULES[@]}" "${BIZ_MODULES[@]}")
|
||
|
||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
|
||
die() { log "FATAL: $*"; exit 1; }
|
||
|
||
# 0) 加载 .env(含 MYSQL_* / REDIS_* / SERVER_PORT / PASSWORD_KEY / SCENSE_ADMIN_*)
|
||
if [ -f "$ENV_FILE" ]; then
|
||
set -a; . "$ENV_FILE"; set +a
|
||
fi
|
||
|
||
MYSQL_ARGS=(--host="${MYSQL_HOST:-127.0.0.1}" --port="${MYSQL_PORT:-3306}"
|
||
--user="${MYSQL_USER:-root}" --password="${MYSQL_PASSWORD:-}")
|
||
DBNAME="${MYSQL_DATABASE:-scense}"
|
||
|
||
# 1) 系统依赖 + venv
|
||
command -v python3 >/dev/null 2>&1 || die "python3 not found"
|
||
command -v mysql >/dev/null 2>&1 || die "mysql client not found"
|
||
[ -d "$VENV_DIR" ] || python3 -m venv "$VENV_DIR"
|
||
# shellcheck disable=SC1091
|
||
. "$VENV_DIR/bin/activate"
|
||
pip install --quiet --upgrade pip
|
||
pip install --quiet xls2ddl
|
||
|
||
# 2) 克隆并安装模块(基础 + 业务)
|
||
mkdir -p "$PKGS_DIR"
|
||
for m in "${ALL_MODULES[@]}"; do
|
||
if [ ! -d "$PKGS_DIR/$m" ]; then
|
||
git clone "https://git.opencomputing.cn/yumoqing/$m.git" "$PKGS_DIR/$m" 2>/dev/null \
|
||
|| git clone "git@git.opencomputing.cn:yumoqing/$m.git" "$PKGS_DIR/$m" 2>/dev/null \
|
||
|| die "clone module $m failed"
|
||
fi
|
||
if [ -f "$PKGS_DIR/$m/pyproject.toml" ]; then
|
||
(cd "$PKGS_DIR/$m" && pip install --quiet -e .)
|
||
fi
|
||
done
|
||
pip install --quiet bricks_for_python
|
||
|
||
# 3) 前端 wwwroot 软链(软链非 cp,保持同步)
|
||
mkdir -p "$WORKDIR/wwwroot"
|
||
for m in "${BIZ_MODULES[@]}"; do
|
||
[ -d "$PKGS_DIR/$m/wwwroot" ] && ln -sfn "../pkgs/$m/wwwroot" "$WORKDIR/wwwroot/$m"
|
||
done
|
||
[ -d "$PKGS_DIR/bricks_for_python/dist" ] && ln -sfn "../pkgs/bricks_for_python/dist" "$WORKDIR/wwwroot/bricks"
|
||
|
||
# 4) i18n 合并(merge_i18n.py 存在则执行,否则跳过不阻断)
|
||
if [ -f "$PKGS_DIR/apppublic/tools/merge_i18n.py" ]; then
|
||
python "$PKGS_DIR/apppublic/tools/merge_i18n.py" "$WORKDIR/wwwroot" "${BIZ_MODULES[@]}" \
|
||
|| log "i18n merge skipped (non-fatal)"
|
||
fi
|
||
|
||
# 5) 数据库:建库 + 各模块 DDL + 应用级 DDL/seed
|
||
mysql "${MYSQL_ARGS[@]}" -e "CREATE DATABASE IF NOT EXISTS \`${DBNAME}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" \
|
||
|| die "create database failed"
|
||
for m in "${BIZ_MODULES[@]}"; do
|
||
if [ -d "$PKGS_DIR/$m/models" ]; then
|
||
(cd "$PKGS_DIR/$m/models" && json2ddl mysql . > "$WORKDIR/mysql.ddl.$m.sql" 2>/dev/null) \
|
||
|| (cd "$PKGS_DIR/$m/models" && xls2ddl mysql . > "$WORKDIR/mysql.ddl.$m.sql" 2>/dev/null) || true
|
||
if [ -f "$WORKDIR/mysql.ddl.$m.sql" ]; then
|
||
mysql "${MYSQL_ARGS[@]}" "$DBNAME" < "$WORKDIR/mysql.ddl.$m.sql" || log "ddl $m warn"
|
||
fi
|
||
fi
|
||
done
|
||
[ -f "$WORKDIR/scripts/ddl.sql" ] && mysql "${MYSQL_ARGS[@]}" "$DBNAME" < "$WORKDIR/scripts/ddl.sql"
|
||
[ -f "$WORKDIR/scripts/seed.sql" ] && mysql "${MYSQL_ARGS[@]}" "$DBNAME" < "$WORKDIR/scripts/seed.sql"
|
||
|
||
# 6) CRUD 生成(xls2ui 从模块 json/ 生成 .dspy + .ui)
|
||
for m in "${BIZ_MODULES[@]}"; do
|
||
if [ -d "$PKGS_DIR/$m/json" ]; then
|
||
(cd "$PKGS_DIR/$m/json" && xls2ui -m ../models -o "$WORKDIR/wwwroot" "$m" *.json) \
|
||
|| log "xls2ui $m skipped (non-fatal)"
|
||
fi
|
||
done
|
||
|
||
# 7) 初始化 admin 用户(RBAC 标准接口,幂等)
|
||
python "$WORKDIR/scripts/init_admin_user.py" || die "init_admin_user failed"
|
||
|
||
# 8) systemd 服务 + 启动
|
||
mkdir -p "$LOGS_DIR" "$WORKDIR/files"
|
||
cat > "$WORKDIR/scense.service" <<EOF
|
||
[Unit]
|
||
Description=scense app (yuanjing)
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=$(whoami)
|
||
WorkingDirectory=$WORKDIR
|
||
EnvironmentFile=$ENV_FILE
|
||
ExecStart=$VENV_DIR/bin/python $WORKDIR/app/scense.py
|
||
Restart=always
|
||
RestartSec=3
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
sudo cp "$WORKDIR/scense.service" /etc/systemd/system/scense.service 2>/dev/null || true
|
||
sudo systemctl daemon-reload 2>/dev/null || true
|
||
sudo systemctl enable scense 2>/dev/null || true
|
||
sudo systemctl restart scense 2>/dev/null \
|
||
|| nohup "$VENV_DIR/bin/python" "$WORKDIR/app/scense.py" >> "$LOGS_DIR/scense.log" 2>&1 &
|
||
|
||
# 9) 端口监听门禁(30s;未监听自动重启一次兜底再等 30s)
|
||
wait_port() {
|
||
local port="$1" tries="${2:-30}"
|
||
for _ in $(seq 1 "$tries"); do
|
||
if (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then
|
||
exec 3>&- 3<&-; return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
return 1
|
||
}
|
||
|
||
if ! wait_port "$APP_PORT" 30; then
|
||
log "WARN: port $APP_PORT not listening in 30s, restart once (bootstrap)"
|
||
sudo systemctl restart scense 2>/dev/null || true
|
||
if ! wait_port "$APP_PORT" 30; then
|
||
log "ERROR: port $APP_PORT still not listening after restart bootstrap"
|
||
tail -n 50 "$LOGS_DIR/scense.log" 2>/dev/null || true
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
log "OK: scense is listening on port $APP_PORT"
|