fix(deploy): 修复 install.sh 同步文件时的长时间卡死
用户实测:执行到「同步程序文件到 /opt/opendance」后卡死。 根因:在源码检出目录执行时,web-server/ 下有 node_modules(236MB / 27359 个文件,admin 下还有 122MB),原实现用 cp -r 全量复制, 随后又对整个目录 chown -R,处理三万个文件表现为卡死。 修复: - 复制改用 copy_tree(),排除 node_modules/.git/storage/dist-release 等 运行期可再生目录;有 rsync 用 rsync -a --delete,否则用 tar 管道回退 - tar 回退的排除模式同时给出 './x' 与 '*/x':只写 './x' 无法匹配嵌套的 './admin/node_modules'(实测会漏掉 122MB,等于没修) - 增加目录安全检查:安装目录与源目录相同、或二者互相包含时直接报错退出, 避免 rm -rf 删掉源文件 / cp 无限递归 - 支持两种目录布局:发布包 admin/ 在根目录,源码检出在 web-server/admin/dist(缺失则现场构建),此前从源码目录执行必然在 「缺少 admin/ 目录」处失败 - 每个目录打印复制结果与文件数,npm 安装前给出预计耗时提示, 不再有长时间无输出的「假卡死」 - 失败时 trap 打印行号 实测(用脚本中真实代码块驱动的用例): 源码检出目录 → /tmp:rsync 路径 1 秒 / 209 文件;tar 回退路径同样 209 文件 发布包解压目录 → /tmp:0 秒,admin 与 dist/main.js 均正确 同目录 / 嵌套目录两种危险布局:均被拒绝并给出可操作提示 两条路径均无 node_modules 泄漏
This commit is contained in:
+85
-8
@@ -20,6 +20,8 @@
|
||||
# SKIP_NGINX=1 只装 API 服务,不配置 nginx
|
||||
# ============================================================
|
||||
set -euo pipefail
|
||||
# 失败时打印出错行号,避免只看到半截输出
|
||||
trap 'printf "\033[1;31m[install]\033[0m 第 %d 行执行失败(退出码 %d)\n" "$LINENO" "$?" >&2' ERR
|
||||
|
||||
SRC_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$SRC_DIR"
|
||||
@@ -71,31 +73,106 @@ if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then
|
||||
|| useradd -r -s /sbin/nologin -d "$APP_DIR" -m "$SERVICE_USER"
|
||||
fi
|
||||
|
||||
log "同步程序文件到 $APP_DIR"
|
||||
# ---- 目录安全检查 ----
|
||||
# 若安装目录与源目录相同或互相包含,后面的 rm -rf / cp 会删掉源文件或无限递归。
|
||||
SRC_REAL="$(readlink -f "$SRC_DIR")"
|
||||
mkdir -p "$APP_DIR"
|
||||
for item in web-server admin contracts; do
|
||||
[ -e "$SRC_DIR/$item" ] || die "发布包缺少 $item/ 目录"
|
||||
APP_REAL="$(readlink -f "$APP_DIR")"
|
||||
|
||||
if [ "$SRC_REAL" = "$APP_REAL" ]; then
|
||||
die "安装目录与源目录相同($APP_REAL)。
|
||||
本脚本会先清空安装目录下的 web-server/ admin/ 再复制,直接在这里执行会删掉源文件。
|
||||
请把发布包解压到别处,或用 APP_DIR=... 指定另一个安装目录:
|
||||
sudo APP_DIR=/opt/opendance bash $0"
|
||||
fi
|
||||
case "$APP_REAL/" in
|
||||
"$SRC_REAL"/*)
|
||||
die "安装目录位于源目录内部($APP_REAL ⊂ $SRC_REAL),复制会无限递归。
|
||||
请改用源目录之外的 APP_DIR。" ;;
|
||||
esac
|
||||
case "$SRC_REAL/" in
|
||||
"$APP_REAL"/*)
|
||||
die "源目录位于安装目录内部($SRC_REAL ⊂ $APP_REAL)。
|
||||
安装时会先清空该子目录,导致源文件被删除。请把发布包解压到 $APP_REAL 之外。" ;;
|
||||
esac
|
||||
|
||||
# ---- 复制(排除 node_modules / .git / storage 等大目录)----
|
||||
# 说明:在 git 检出目录直接执行时,web-server/ 里可能已有 200 MB+ 的
|
||||
# node_modules(数万个小文件)。原实现用 cp -r 全量复制,会长时间无输出,
|
||||
# 看起来像卡死。这里统一排除,运行期依赖稍后由 npm ci 重新安装。
|
||||
COPY_EXCLUDES=(--exclude=node_modules --exclude=.git --exclude=storage
|
||||
--exclude=dist-release --exclude=coverage --exclude=.gradle)
|
||||
|
||||
copy_tree() {
|
||||
local src="$1" dst="$2"
|
||||
mkdir -p "$dst"
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -a --delete "${COPY_EXCLUDES[@]}" "$src/" "$dst/"
|
||||
else
|
||||
# 无 rsync 时用 tar 管道,同样支持排除。
|
||||
# 注意:tar 的 --exclude 按完整成员名匹配,只写 "./node_modules" 无法匹配
|
||||
# 嵌套的 "./admin/node_modules"(实测会漏掉 admin 下的 122MB 依赖)。
|
||||
# 因此每个名字都要同时给出顶层与任意层级的模式。
|
||||
local tar_excludes=()
|
||||
for pattern in node_modules .git storage dist-release coverage .gradle; do
|
||||
tar_excludes+=(--exclude="./$pattern" --exclude="*/$pattern")
|
||||
done
|
||||
( cd "$src" && tar "${tar_excludes[@]}" -cf - . ) | ( cd "$dst" && tar -xf - )
|
||||
fi
|
||||
}
|
||||
|
||||
log "同步程序文件到 $APP_DIR"
|
||||
for item in web-server contracts; do
|
||||
[ -e "$SRC_DIR/$item" ] || die "缺少 $item/ 目录(既不在发布包里,也不在源码根目录)"
|
||||
printf ' 复制 %-12s …' "$item"
|
||||
rm -rf "${APP_DIR:?}/$item"
|
||||
cp -r "$SRC_DIR/$item" "$APP_DIR/$item"
|
||||
copy_tree "$SRC_DIR/$item" "$APP_DIR/$item"
|
||||
printf ' %s 个文件\n' "$(find "$APP_DIR/$item" -type f | wc -l)"
|
||||
done
|
||||
|
||||
# ---- 管理端产物:两种目录布局都要支持 ----
|
||||
# 发布包: <root>/admin/ (已构建)
|
||||
# 源码检出:<root>/web-server/admin/dist/ (需已构建,缺失则现场构建)
|
||||
ADMIN_SRC=""
|
||||
if [ -d "$SRC_DIR/admin" ]; then
|
||||
ADMIN_SRC="$SRC_DIR/admin"
|
||||
elif [ -d "$SRC_DIR/web-server/admin/dist" ]; then
|
||||
ADMIN_SRC="$SRC_DIR/web-server/admin/dist"
|
||||
elif [ -d "$SRC_DIR/web-server/admin" ]; then
|
||||
warn "未找到管理端构建产物,正在构建 web-server/admin(需要 devDependencies)"
|
||||
if [ -f "$SRC_DIR/web-server/admin/package-lock.json" ]; then
|
||||
( cd "$SRC_DIR/web-server/admin" && npm ci --no-audit --no-fund --loglevel=error )
|
||||
else
|
||||
( cd "$SRC_DIR/web-server/admin" && npm install --no-audit --no-fund --loglevel=error )
|
||||
fi
|
||||
( cd "$SRC_DIR/web-server/admin" && npm run build )
|
||||
ADMIN_SRC="$SRC_DIR/web-server/admin/dist"
|
||||
fi
|
||||
|
||||
[ -n "$ADMIN_SRC" ] || die "找不到管理端:既没有 admin/ 目录,也没有 web-server/admin/"
|
||||
printf ' 复制 %-12s …' "admin"
|
||||
rm -rf "${APP_DIR:?}/admin"
|
||||
copy_tree "$ADMIN_SRC" "$APP_DIR/admin"
|
||||
printf ' %s 个文件(来源:%s)\n' "$(find "$APP_DIR/admin" -type f | wc -l)" "$ADMIN_SRC"
|
||||
|
||||
mkdir -p "$APP_DIR/deploy"
|
||||
cp -r "$SRC_DIR/deploy/." "$APP_DIR/deploy/"
|
||||
if [ -f "$SRC_DIR/THIRD_PARTY.md" ]; then cp "$SRC_DIR/THIRD_PARTY.md" "$APP_DIR/"; fi
|
||||
if [ -f "$SRC_DIR/README.txt" ]; then cp "$SRC_DIR/README.txt" "$APP_DIR/"; fi
|
||||
|
||||
mkdir -p "$STORAGE_DIR"
|
||||
log "设置目录属主($SERVICE_USER)"
|
||||
chown -R "$SERVICE_USER:$SERVICE_USER" "$APP_DIR" "$STORAGE_DIR"
|
||||
chmod 750 "$STORAGE_DIR"
|
||||
|
||||
# ---------- 2. 安装运行期依赖(必要时构建) ----------
|
||||
cd "$APP_DIR/web-server"
|
||||
|
||||
log "安装运行期依赖(仅生产依赖,可能需要 1-3 分钟…)"
|
||||
if [ -f package-lock.json ]; then
|
||||
log "安装运行期依赖(npm ci --omit=dev)"
|
||||
sudo -u "$SERVICE_USER" npm ci --omit=dev --no-audit --no-fund
|
||||
sudo -u "$SERVICE_USER" npm ci --omit=dev --no-audit --no-fund --loglevel=error
|
||||
else
|
||||
log "安装运行期依赖(npm install --omit=dev)"
|
||||
sudo -u "$SERVICE_USER" npm install --omit=dev --no-audit --no-fund
|
||||
sudo -u "$SERVICE_USER" npm install --omit=dev --no-audit --no-fund --loglevel=error
|
||||
fi
|
||||
|
||||
if [ ! -f dist/main.js ]; then
|
||||
|
||||
Reference in New Issue
Block a user