#!/usr/bin/env bash # 织忆 MemoryWeave 统一部署脚本 — build two + install two + restart two + verify # 2026-10-08 · t_f57b8bbf · 根治「重启 zhiyid 不重启 sidecar → Rust 改部署后 sidecar 跑旧二进制静默故障」事故 # # 用法: # ./scripts/deploy.sh 完整部署: 构建两+装两+停两+启两+校验 # ./scripts/deploy.sh --verify-only 只跑校验(不构建/不安装/不重启)—— 反例测试/人工体检用 # make deploy = ./scripts/deploy.sh set -euo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BIN_DIR="${BIN_DIR:-/home/muc/bin}" # 构建产物(持久路径,均已 gitignore) BUILD_GO="$REPO_DIR/go/cmd/zhiyid/zhiyid" BUILD_RUST="$REPO_DIR/rust/target/release/zhiyi-consolidate" # 安装落点(与 ~/.config/systemd/user/*.service 的 ExecStart 一致) INSTALL_GO="$BIN_DIR/zhiyid-new" INSTALL_RUST="$BIN_DIR/zhiyi-consolidate" # 服务名(用户级 systemd) SVC_GO="zhiyid.service" SVC_RUST="zhiyi-consolidate.service" HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:7821/health}" log() { printf '\033[1;34m[deploy]\033[0m %s\n' "$*"; } die() { printf '\033[1;31m[deploy:FAIL]\033[0m %s\n' "$*" >&2; exit 1; } # 校验单个服务的运行态是否=本次构建的二进制 # 判据优先级(纪律): 副作用(PID/inode/校验和) > 返回值 > 自报 verify_service() { local name="$1" build_path="$2" pid exe run_sha build_sha # ① is-active [ "$(systemctl --user is-active "$name")" = "active" ] || die "$name 非 active" # ② 取 MainPID pid="$(systemctl --user show -p MainPID --value "$name")" [ -n "$pid" ] && [ "$pid" != "0" ] || die "$name: MainPID 为空/0" # ③ 进程 exe 不得指向已删除 inode(旧二进制被 install 覆盖后、进程仍跑旧 inode → readlink 含 "(deleted)") exe="$(readlink "/proc/$pid/exe" 2>/dev/null || true)" case "$exe" in "") die "$name: 无法 readlink /proc/$pid/exe" ;; *" (deleted)"*) die "$name: 进程仍跑已删除 inode → 二进制被换过但进程没重启 ($exe)" ;; esac # ④ 运行中二进制校验和 == 本次构建产物(抓「磁盘装了新版但进程仍跑旧版」静默故障) run_sha="$(sha256sum "/proc/$pid/exe" 2>/dev/null | awk '{print $1}')" build_sha="$(sha256sum "$build_path" 2>/dev/null | awk '{print $1}')" [ -n "$run_sha" ] || die "$name: 无法读取运行中二进制" [ "$run_sha" = "$build_sha" ] || die "$name: 运行中二进制 ≠ 本次构建产物 (运行=$run_sha 构建=$build_sha) → sidecar 静默跑旧版" log "✔ $name active · PID=$pid · exe=$exe · 校验和匹配本次构建" } # 深健康检查: /health 连 sidecar 可达性(P0 已做,故障返 503)→ 期望 200 verify_health() { local code code="$(curl -s -o /dev/null -w '%{http_code}' -m 5 "$HEALTH_URL")" [ "$code" = "200" ] || die "/health 返回 $code (期望 200) → zhiyid 或 sidecar 故障" log "✔ /health = $code" } do_build() { log "构建 Go → $BUILD_GO" ( cd "$REPO_DIR/go" && CGO_ENABLED=1 go build -o cmd/zhiyid/zhiyid ./cmd/zhiyid ) log "构建 Rust → $BUILD_RUST" ( cd "$REPO_DIR/rust" && cargo build --release ) } do_install() { log "停止 $SVC_GO + $SVC_RUST" systemctl --user stop "$SVC_GO" || true systemctl --user stop "$SVC_RUST" || true # 用 install 而非 cp:install 先写临时再 rename,避免 Text file busy(进程占着旧 inode 时 cp 会 EBUSY) log "install 二进制(不用 cp,避免 Text file busy)" install -m 0755 "$BUILD_GO" "$INSTALL_GO" install -m 0755 "$BUILD_RUST" "$INSTALL_RUST" log "daemon-reload" systemctl --user daemon-reload log "启动 $SVC_GO + $SVC_RUST" systemctl --user start "$SVC_GO" systemctl --user start "$SVC_RUST" sleep 2 } do_verify() { log "校验 ..." verify_service "$SVC_GO" "$BUILD_GO" verify_service "$SVC_RUST" "$BUILD_RUST" verify_health log "✅ 部署成功: 两服务 active · 进程无 (deleted) · 运行二进制=本次构建 · /health=200" } if [ "${1:-}" = "--verify-only" ]; then do_verify exit 0 fi do_build do_install do_verify