From 7cd607ecde17fbd7ccd024b04b0965ba72e70c3f Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Tue, 15 Sep 2026 00:14:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=E4=BF=AE=E5=A4=8D=20install.sh?= =?UTF-8?q?=20=E5=90=8C=E6=AD=A5=E6=96=87=E4=BB=B6=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E9=95=BF=E6=97=B6=E9=97=B4=E5=8D=A1=E6=AD=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户实测:执行到「同步程序文件到 /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 泄漏 --- deploy/install.sh | 93 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 8 deletions(-) diff --git a/deploy/install.sh b/deploy/install.sh index b206133..63d3c41 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -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 + +# ---- 管理端产物:两种目录布局都要支持 ---- +# 发布包: /admin/ (已构建) +# 源码检出:/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