feat: add service config templates and extraction script

Former-commit-id: 1de24b7eb79676d1aba9d799a58c5a753290cf52
This commit is contained in:
反编译工作区
2026-05-01 19:38:01 +08:00
parent 3175b7074b
commit 8b15445328
2433 changed files with 8322164 additions and 1604 deletions
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# V1 基准与当前构建 fat-jar 的嵌套 lib 坐标 multiset 门禁。见 scripts/generate_v1_v2_elevator_dependency_diff.py --help
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
exec python3 "$ROOT/scripts/generate_v1_v2_elevator_dependency_diff.py" --gate "$@"
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env bash
set -euo pipefail
# 生产只读证据采集(Maven 发布包内与本脚本同置于部署根目录,与 start.sh / properties 同层):
# - 进程参数/环境/工作目录
# - 本地配置文件
# - jcmd system properties
# - 应用日志关键片段
# - Consul 健康与 KV 快照
# 最终输出 tar.gz,便于离线定位“配置来源 -> Ribbon 实例列表”问题。
APP_DIR="${1:-/data/cwos/cw-elevator-application-V1.0.0.20211103}"
CONSUL_ADDR="${2:-10.0.22.102:8500}"
OUT_ROOT="${3:-${APP_DIR}/evidence}"
APP_NAME="${4:-elevator-app}"
# 现场 JDK(生产 cwos-node 固定路径;不依赖 PATH
CWOS_JAVA_BIN="/data/cwos/java/bin"
JAVA_BIN="${CWOS_JAVA_BIN}/java"
JAR_BIN="${CWOS_JAVA_BIN}/jar"
JCMD_BIN="${CWOS_JAVA_BIN}/jcmd"
DATE_BIN="/bin/date"
timestamp="$(${DATE_BIN} +%Y%m%d-%H%M%S)"
OUT_DIR="${OUT_ROOT}/elevator-evidence-${timestamp}"
mkdir -p "${OUT_DIR}"
log() { echo "[collect] $*"; }
log "APP_DIR=${APP_DIR}"
log "CONSUL_ADDR=${CONSUL_ADDR}"
log "OUT_DIR=${OUT_DIR}"
log "JAVA_BIN=${JAVA_BIN} JAR_BIN=${JAR_BIN} JCMD_BIN=${JCMD_BIN}"
PID="$(ps -ef | awk '/java/ && /cw-elevator-application/ && !/awk/ {print $2; exit}')"
if [[ -z "${PID}" ]]; then
echo "ERROR: 未找到 cw-elevator-application Java 进程" >&2
exit 1
fi
log "PID=${PID}"
echo "${PID}" > "${OUT_DIR}/pid.txt"
# 1) 进程与系统基础信息
ps -ef > "${OUT_DIR}/ps-ef.txt"
uname -a > "${OUT_DIR}/uname.txt"
${DATE_BIN} +%Y-%m-%dT%H:%M:%S%z > "${OUT_DIR}/collected-at.txt"
tr '\0' ' ' < "/proc/${PID}/cmdline" > "${OUT_DIR}/proc-cmdline.txt" || true
tr '\0' '\n' < "/proc/${PID}/environ" > "${OUT_DIR}/proc-environ.txt" || true
ls -l "/proc/${PID}/cwd" > "${OUT_DIR}/proc-cwd.txt" || true
# 2) 本地配置快照(若存在)
for f in bootstrap.properties application.properties application-access-control.properties start.sh stop.sh cw-elevator-application.service; do
if [[ -f "${APP_DIR}/${f}" ]]; then
cp -a "${APP_DIR}/${f}" "${OUT_DIR}/${f}"
fi
done
# 3) JAR 与结构快照
JAR_PATH="$(awk '{print $1}' "${OUT_DIR}/proc-cmdline.txt" | sed 's/[[:space:]]*$//')"
if [[ -f "${APP_DIR}/cw-elevator-application-V1.0.0.20211103.jar" ]]; then
JAR_PATH="${APP_DIR}/cw-elevator-application-V1.0.0.20211103.jar"
fi
echo "${JAR_PATH}" > "${OUT_DIR}/jar-path.txt"
if [[ -f "${JAR_PATH}" ]]; then
sha256sum "${JAR_PATH}" > "${OUT_DIR}/jar.sha256.txt" || true
if [[ -x "${JAR_BIN}" ]]; then
"${JAR_BIN}" tf "${JAR_PATH}" > "${OUT_DIR}/jar-tf.txt" || true
else
echo "jar not found or not executable: ${JAR_BIN}" > "${OUT_DIR}/jar-tf.txt"
fi
unzip -p "${JAR_PATH}" application.properties > "${OUT_DIR}/jar-application.properties.txt" 2>/dev/null || true
unzip -p "${JAR_PATH}" bootstrap.properties > "${OUT_DIR}/jar-bootstrap.properties.txt" 2>/dev/null || true
fi
# 4) jcmd system properties + attach 诊断(不修改应用配置;便于修复 AttachNotSupportedException
{
echo "=== current shell user ==="
id 2>/dev/null || true
echo "=== target java process ==="
ps -o user=,group=,pid=,args= -p "${PID}" 2>/dev/null || true
PROC_USER="$(stat -c '%U' "/proc/${PID}" 2>/dev/null || echo "")"
PROC_UID="$(stat -c '%u' "/proc/${PID}" 2>/dev/null || echo "")"
echo "proc_owner=${PROC_USER} uid=${PROC_UID}"
echo "=== /tmp hsperfdata (HotSpot perf counter; attach 相关) ==="
if [[ -n "${PROC_USER}" && "${PROC_USER}" != "unknown" ]]; then
HS="/tmp/hsperfdata_${PROC_USER}"
if [[ -d "${HS}" ]]; then
ls -la "${HS}" 2>/dev/null | head -30 || true
ls -la "${HS}/${PID}" 2>/dev/null || echo "missing ${HS}/${PID}"
else
echo "no directory ${HS}"
fi
fi
echo "=== cmdline tokens (attach / jdwp) ==="
tr '\0' '\n' < "/proc/${PID}/cmdline" 2>/dev/null | grep -E 'DisableAttach|Attach|jdwp|agentpath' || echo "(none matched)"
} > "${OUT_DIR}/jcmd-attach-diagnose.txt" 2>&1
if [[ -x "${JCMD_BIN}" ]]; then
"${JCMD_BIN}" "${PID}" VM.system_properties > "${OUT_DIR}/jcmd-system-properties.txt" 2>&1 || true
if grep -q 'AttachNotSupportedException\|Unable to open socket file' "${OUT_DIR}/jcmd-system-properties.txt" 2>/dev/null; then
{
echo ""
echo "HINT: jcmd attach 失败常见原因:"
echo " 1) 与 Java 进程不同用户执行 jcmd(请用与进程相同用户,例如: sudo -u <java_user> ${JCMD_BIN} ${PID} VM.system_properties"
echo " 2) /tmp/hsperfdata_<user>/<pid> 缺失或权限异常"
echo " 3) JVM 启动参数含 -XX:+DisableAttachMechanism(见 jcmd-attach-diagnose.txt 中 cmdline"
echo " 4) 进程非 HotSpot 或尚未完全初始化(极少见于长期运行的 Spring Boot"
} >> "${OUT_DIR}/jcmd-system-properties.txt"
fi
else
echo "jcmd not found or not executable: ${JCMD_BIN}" > "${OUT_DIR}/jcmd-system-properties.txt"
fi
# 4b) java 版本(与现场 JDK 一致性的旁证)
if [[ -x "${JAVA_BIN}" ]]; then
"${JAVA_BIN}" -version > "${OUT_DIR}/java-version.txt" 2>&1 || true
else
echo "java not found or not executable: ${JAVA_BIN}" > "${OUT_DIR}/java-version.txt"
fi
# 5) 应用日志关键行
LOG_FILE="${APP_DIR}/logs/elevator-app.log"
if [[ -f "${LOG_FILE}" ]]; then
cp -a "${LOG_FILE}" "${OUT_DIR}/elevator-app.log.full"
awk '
/CONFIG SOURCE PROBE START|CONFIG SOURCE PROBE END|probe key=|ConfigurationBasedServerList|Load balancer does not have available server|DynamicServerListLoadBalancer|ConsulServiceRegistry|Registering service with consul/ { print }
' "${LOG_FILE}" > "${OUT_DIR}/elevator-app.log.keylines.txt"
fi
# 6) Consul 快照
CURL="curl -sS --max-time 8"
${CURL} "http://${CONSUL_ADDR}/v1/health/service/${APP_NAME}?passing=true" > "${OUT_DIR}/consul-health-${APP_NAME}.json" || true
for svc in cwos-portal ninca-common ninca-common-component-organization ninca-crk-std cloudwalk-device-thirdparty; do
${CURL} "http://${CONSUL_ADDR}/v1/health/service/${svc}?passing=true" > "${OUT_DIR}/consul-health-${svc}.json" || true
done
${CURL} "http://${CONSUL_ADDR}/v1/kv/config/${APP_NAME}/data?raw" > "${OUT_DIR}/consul-kv-${APP_NAME}.properties" || true
${CURL} "http://${CONSUL_ADDR}/v1/kv/config/${APP_NAME},access-control/data?raw" > "${OUT_DIR}/consul-kv-${APP_NAME},access-control.properties" || true
# 7) 现场可达性快照(已知主机名)
for host in 0837a70b5fab47569391828f5feb2561 371bfca4972c43d2aefcf302d0a4a277 44700995ee904679a7ad5afddcf93bb5; do
getent hosts "${host}" > "${OUT_DIR}/getent-${host}.txt" 2>&1 || true
curl -I --max-time 5 "http://${host}:8089/" > "${OUT_DIR}/curl-head-${host}-8089.txt" 2>&1 || true
done
ARCHIVE="${OUT_DIR}.tar.gz"
tar -czf "${ARCHIVE}" -C "$(dirname "${OUT_DIR}")" "$(basename "${OUT_DIR}")"
log "DONE archive=${ARCHIVE}"
echo "${ARCHIVE}"
+269
View File
@@ -0,0 +1,269 @@
#!/usr/bin/env python3
"""
Extract listed ninca-crk-* lib jars from fat jar, CFR-decompile each into maven-ninca-crk-from-lib/<artifactId>/,
emit parent + child POMs (embedded pom from jar when present, sanitized parent).
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
FAT_JAR_DEFAULT = Path(
"/media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/"
"星河湾星中星/星中心/ninca_crk_std_01-ninca_crk_std_backend/ninca-crk-std-backend-V2.9.2_20210730.jar"
)
REPO_ROOT = Path(__file__).resolve().parents[1]
OUT_ROOT = REPO_ROOT / "maven-ninca-crk-from-lib"
CFR_JAR = Path("/tmp/cfr-0.152.jar")
TARGET_NAMES = [
"ninca-crk-access-control-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-web-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-web-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-web-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-web-2.9.1_210630-SNAPSHOT.jar",
]
GROUP_ID = "cn.cloudwalk.ninca"
REACTOR_VERSION = "2.9.1_210630-SNAPSHOT"
JAVA_VERSION = "1.8"
def artifact_dir(jar_name: str) -> str:
return jar_name.replace(".jar", "").replace("-2.9.1_210630-SNAPSHOT", "")
def ensure_cfr() -> Path:
if CFR_JAR.is_file():
return CFR_JAR
raise SystemExit(
"Missing CFR at {} — download:\n"
" curl -fsSL -o {} "
"https://github.com/leibnitz27/cfr/releases/download/0.152/cfr-0.152.jar".format(CFR_JAR, CFR_JAR)
)
def extract_embedded_pom(zf: zipfile.ZipFile, inner_path: str) -> str | None:
try:
return zf.read(inner_path).decode("utf-8", errors="replace")
except KeyError:
return None
def find_embedded_pom_xml(zf: zipfile.ZipFile) -> tuple[str | None, str | None]:
"""Return (pom_xml_content, relative_path_in_zip)."""
for n in zf.namelist():
if n.endswith("/pom.xml") and "/META-INF/maven/" in n and "/ninca-crk-" in n:
return extract_embedded_pom(zf, n), n
return None, None
def sanitize_pom_xml(raw: str, artifact_id: str) -> str:
"""Remove embedded parent; attach reactor parent after modelVersion."""
raw = raw.lstrip("\ufeff").strip()
raw = re.sub(r"<parent\b[^>]*>[\s\S]*?</parent>\s*", "", raw, count=1, flags=re.I)
parent_block = (
" <parent>\n"
" <groupId>%s</groupId>\n"
" <artifactId>ninca-crk-from-lib-reactor</artifactId>\n"
" <version>%s</version>\n"
" <relativePath>../pom.xml</relativePath>\n"
" </parent>\n" % (GROUP_ID, REACTOR_VERSION)
)
m = re.search(r"</modelVersion>\s*", raw, flags=re.I)
if not m:
raise ValueError("embedded pom missing modelVersion")
pos = m.end()
return raw[:pos] + parent_block + raw[pos:]
def minimal_pom(artifact_id: str, description: str) -> str:
return """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>{gid}</groupId>
<artifactId>ninca-crk-from-lib-reactor</artifactId>
<version>{ver}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>{aid}</artifactId>
<name>{aid}</name>
<description>{desc}</description>
<properties>
<maven.compiler.source>{jv}</maven.compiler.source>
<maven.compiler.target>{jv}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
""".format(
gid=GROUP_ID,
ver=REACTOR_VERSION,
aid=artifact_id,
desc=description,
jv=JAVA_VERSION,
)
def reactor_pom(module_dirs: list[str]) -> str:
mods = "".join(" <module>{}</module>\n".format(m) for m in module_dirs)
return """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>{gid}</groupId>
<artifactId>ninca-crk-from-lib-reactor</artifactId>
<version>{ver}</version>
<packaging>pom</packaging>
<name>ninca-crk-from-lib-reactor</name>
<description>
CFR 反编译自交付 fat jar lib/ 内 ninca-crk 四条业务线(access-control / conference-attendance /
smart-attendance / visitor-management)共 28 个构件;用于源码走查与对照,不代表官方原始工程结构。
</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>{jv}</java.version>
<maven.compiler.source>${{java.version}}</maven.compiler.source>
<maven.compiler.target>${{java.version}}</maven.compiler.target>
</properties>
<modules>
{mods} </modules>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${{java.version}}</source>
<target>${{java.version}}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
""".format(
gid=GROUP_ID,
ver=REACTOR_VERSION,
mods=mods,
jv=JAVA_VERSION,
)
def run_cfr(jar_path: Path, java_out: Path) -> None:
java_out.mkdir(parents=True, exist_ok=True)
cmd = [
"java",
"-jar",
str(ensure_cfr()),
str(jar_path),
"--outputdir",
str(java_out),
"--silent",
"true",
]
subprocess.run(cmd, check=True)
def main() -> int:
fat_jar = Path(sys.argv[1]) if len(sys.argv) > 1 else FAT_JAR_DEFAULT
if not fat_jar.is_file():
print("Fat jar not found:", fat_jar, file=sys.stderr)
return 1
if OUT_ROOT.exists():
shutil.rmtree(OUT_ROOT)
OUT_ROOT.mkdir(parents=True)
module_dirs: list[str] = []
tmpdir = Path(tempfile.mkdtemp(prefix="crk_lib_extract_"))
try:
with zipfile.ZipFile(fat_jar, "r") as zf:
for name in TARGET_NAMES:
inner = "lib/" + name
if inner not in zf.namelist():
print("MISSING in fat jar:", inner, file=sys.stderr)
return 2
extract_path = tmpdir / name
with zf.open(inner) as src, open(extract_path, "wb") as dst:
shutil.copyfileobj(src, dst)
aid = artifact_dir(name)
mod_path = OUT_ROOT / aid
mod_path.mkdir(parents=True)
java_root = mod_path / "src" / "main" / "java"
java_root.mkdir(parents=True)
run_cfr(extract_path, java_root)
with zipfile.ZipFile(extract_path, "r") as inner_zf:
embedded, emb_path = find_embedded_pom_xml(inner_zf)
desc = "CFR from {} (embedded pom: {})".format(name, emb_path or "none")
if embedded:
try:
pom_body = sanitize_pom_xml(embedded, aid)
(mod_path / "pom.xml").write_text(pom_body, encoding="utf-8")
except Exception as exc:
print("WARN sanitize pom:", aid, exc, file=sys.stderr)
(mod_path / "pom.xml").write_text(minimal_pom(aid, desc), encoding="utf-8")
else:
(mod_path / "pom.xml").write_text(minimal_pom(aid, desc), encoding="utf-8")
module_dirs.append(aid)
(OUT_ROOT / "pom.xml").write_text(reactor_pom(module_dirs), encoding="utf-8")
readme = OUT_ROOT / "README.txt"
readme.write_text(
"Generated by scripts/decompile_ninca_crk_lib_modules.py\n"
"Source fat jar: {}\n".format(fat_jar)
+ "CFR: {}\n".format(CFR_JAR)
+ "\n反编译产物仅供走查;首次编译需在私服可用的环境下补齐依赖。\n",
encoding="utf-8",
)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
print("OK ->", OUT_ROOT)
return 0
if __name__ == "__main__":
sys.exit(main())
+555
View File
@@ -0,0 +1,555 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""从 V1/V2 fat-jar 二进制解析嵌套 lib,读取 META-INF/maven/**/pom.properties,生成与 Maven 坐标可比对的数据。
子命令/模式:
- 默认:写 docs/testing/cw-elevator-v1-v2-dependency-diff.md
- --gate:以 V1 为基准,对 candidate fat-jar 做嵌套 JAR 坐标 multiset 比对,不一致时非零退出(可配允许列表)。
"""
from __future__ import annotations
import argparse
import io
import sys
import zipfile
from collections import Counter, defaultdict
from pathlib import Path
def _parse_props(raw: str) -> dict[str, str]:
props: dict[str, str] = {}
for line in raw.splitlines():
line = line.strip()
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("=")
props[k.strip()] = v.strip()
return props
def coords_from_nested_jar(data: bytes, jar_basename: str) -> tuple[str, str, str]:
"""Returns (groupId, artifactId, version) using META-INF/maven/**/pom.properties."""
try:
z = zipfile.ZipFile(io.BytesIO(data))
except zipfile.BadZipFile:
return ("?", "?", "?")
candidates: list[tuple[str, str, str]] = []
for name in z.namelist():
if not name.endswith("/pom.properties"):
continue
if "META-INF/maven/" not in name:
continue
try:
raw = z.read(name).decode("utf-8", errors="replace")
except KeyError:
continue
props = _parse_props(raw)
gid = props.get("groupId")
aid = props.get("artifactId")
ver = props.get("version")
if gid and aid and ver:
candidates.append((gid, aid, ver))
if not candidates:
return ("?", "?", "?")
stem = jar_basename[: -len(".jar")] if jar_basename.endswith(".jar") else jar_basename
for g, a, v in candidates:
if stem.startswith(a) or a in stem or stem.startswith(a.split(".")[-1]):
return (g, a, v)
return candidates[0]
def list_outer_nested(
outer: Path, inner_dir_prefix: str
) -> dict[str, tuple[str, str, str]]:
"""path_in_zip -> (g,a,v)"""
result: dict[str, tuple[str, str, str]] = {}
z = zipfile.ZipFile(outer)
for name in z.namelist():
if not name.startswith(inner_dir_prefix):
continue
if not name.endswith(".jar"):
continue
if name.endswith(".jar.original"):
continue
try:
data = z.read(name)
except KeyError:
continue
base = Path(name).name
g, a, v = coords_from_nested_jar(data, base)
result[name] = (g, a, v)
return result
def detect_lib_prefix(outer: Path) -> str:
"""可执行包为 V1 风格 lib/ 或 Spring Boot repackage 的 BOOT-INF/lib/。"""
z = zipfile.ZipFile(outer)
has_boot = any(n.startswith("BOOT-INF/lib/") and n.endswith(".jar") for n in z.namelist())
has_lib = any(n.startswith("lib/") and n.endswith(".jar") for n in z.namelist())
if has_boot and not has_lib:
return "BOOT-INF/lib/"
if has_lib:
return "lib/"
return "lib/"
def key_ga(path: str, t: tuple[str, str, str]) -> str:
g, a, v = t
if g == "?" or a == "?" or v == "?":
return f"unresolved:{Path(path).name}"
return f"{g}:{a}:{v}"
def multiset_from_jar(outer: Path) -> tuple[Counter[str], str, dict[str, tuple[str, str, str]]]:
prefix = detect_lib_prefix(outer)
jmap = list_outer_nested(outer, prefix)
ctr: Counter[str] = Counter()
for path, t in jmap.items():
ctr[key_ga(path, t)] += 1
return ctr, prefix, jmap
def _read_allow_counter(path: Path | None) -> Counter[str]:
if path is None or not path.is_file():
return Counter()
c: Counter[str] = Counter()
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split()
if len(parts) >= 2 and parts[-1].isdigit():
key, cnt = " ".join(parts[:-1]), int(parts[-1])
else:
key, cnt = line, 1
c[key] += cnt
return c
def _subtract_allow(diff: Counter[str], allow: Counter[str]) -> Counter[str]:
"""从 multiset 差值中扣减允许的盈余额度(每行 `coord` 或 `coord N`)。"""
out = Counter(diff)
for k, n in allow.items():
if k not in out:
continue
out[k] -= min(out[k], n)
if out[k] <= 0:
del out[k]
return out
def run_lib_gate(
baseline_jar: Path,
candidate_jar: Path,
allow_baseline_only: Path | None,
allow_candidate_only: Path | None,
) -> int:
"""Multiset 门禁:在应用允许偏差后 baseline 与 candidate 须一致。"""
if not baseline_jar.is_file():
print("Missing baseline jar:", baseline_jar, file=sys.stderr)
return 2
if not candidate_jar.is_file():
print("Missing candidate jar:", candidate_jar, file=sys.stderr)
return 2
b_ctr, b_prefix, _ = multiset_from_jar(baseline_jar)
c_ctr, c_prefix, _ = multiset_from_jar(candidate_jar)
allow_b = _read_allow_counter(allow_baseline_only)
allow_c = _read_allow_counter(allow_candidate_only)
only_b = _subtract_allow(b_ctr - c_ctr, allow_b)
only_c = _subtract_allow(c_ctr - b_ctr, allow_c)
if not only_b and not only_c:
print(
"lib parity OK:",
f"baseline={baseline_jar.name} ({b_prefix} n={sum(b_ctr.values())})",
f"candidate={candidate_jar.name} ({c_prefix} n={sum(c_ctr.values())})",
)
return 0
print("lib parity FAILED (multiset diff after allowlists)", file=sys.stderr)
print(f" baseline: {baseline_jar} prefix={b_prefix} keys={len(b_ctr)}", file=sys.stderr)
print(f" candidate: {candidate_jar} prefix={c_prefix} keys={len(c_ctr)}", file=sys.stderr)
if only_b:
print(" only in baseline (or excess count):", file=sys.stderr)
for k in sorted(only_b.keys()):
print(f" {k} x{only_b[k]}", file=sys.stderr)
if only_c:
print(" only in candidate (or excess count):", file=sys.stderr)
for k in sorted(only_c.keys()):
print(f" {k} x{only_c[k]}", file=sys.stderr)
return 1
def parse_maven_dependency_list(path: Path) -> list[tuple[str, str, str, str]]:
"""Parse mvn dependency:list output lines like 'groupId:artifactId:jar:version:compile'."""
rows: list[tuple[str, str, str, str]] = []
text = path.read_text(encoding="utf-8", errors="replace")
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("The following"):
continue
if line == "none":
continue
parts = line.split(":")
if len(parts) >= 5 and parts[2] == "jar":
gid, aid, _, ver, scope = parts[0], parts[1], parts[2], parts[3], parts[4]
rows.append((gid, aid, ver, scope))
return sorted(rows, key=lambda x: (x[0], x[1], x[2]))
def collect_ga_versions_from_jar_map(
jmap: dict[str, tuple[str, str, str]],
) -> dict[tuple[str, str], set[str]]:
"""嵌套 jar -> 每个 (groupId, artifactId) 在 fat-jar 中出现的 version 集合(仅 pom.properties 可解析条目)。"""
ga_ver: dict[tuple[str, str], set[str]] = defaultdict(set)
for _path, (g, a, v) in jmap.items():
if g != "?" and a != "?" and v != "?":
ga_ver[(g, a)].add(v)
return ga_ver
def ga_version_skew_rows(
v1_ga_v: dict[tuple[str, str], set[str]],
v2_ga_v: dict[tuple[str, str], set[str]],
) -> list[tuple[tuple[str, str], list[str], list[str]]]:
"""两侧均能解析到 version、且 version 集合不一致的 GA。"""
out: list[tuple[tuple[str, str], list[str], list[str]]] = []
all_ga = set(v1_ga_v.keys()) | set(v2_ga_v.keys())
for ga in sorted(all_ga, key=lambda x: (x[0], x[1])):
s1 = v1_ga_v.get(ga, set())
s2 = v2_ga_v.get(ga, set())
if s1 and s2 and s1 != s2:
out.append((ga, sorted(s1), sorted(s2)))
return out
def main() -> int:
root = Path(__file__).resolve().parents[1]
ap = argparse.ArgumentParser(description="V1/V2 fat-jar 依赖对比或 lib multiset 门禁")
ap.add_argument(
"--gate",
action="store_true",
help="运行 multiset 门禁(默认基准 V1 jar,候选为 releases 或 starter/target",
)
ap.add_argument("--baseline-jar", type=Path, default=None, help="门禁基准 fat-jar")
ap.add_argument("--candidate-jar", type=Path, default=None, help="门禁待测 fat-jar")
ap.add_argument(
"--allow-baseline-only",
type=Path,
default=None,
help="允许仅出现在 baseline 的坐标(每行 `g:a:v` 或 `unresolved:name.jar`,可选末尾计数)",
)
ap.add_argument(
"--allow-candidate-only",
type=Path,
default=None,
help="允许仅出现在 candidate 的坐标(格式同上)",
)
args = ap.parse_args()
if args.gate:
b = (
args.baseline_jar
if args.baseline_jar
else root
/ "cw-elevator-application-V1.0.0.20211103"
/ "cw-elevator-application-V1.0.0.20211103.jar"
)
cand = args.candidate_jar
if cand is None:
# 优先本地 package 产物(与当前父 POM / 插件一致);其次历史 releases 归档
cand = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.7.jar"
)
if not cand.is_file():
cand = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.0.jar"
)
if not cand.is_file():
cand = (
root
/ "maven-cw-elevator-application"
/ "releases"
/ "cw-elevator-application-V2.0.6.20260430"
/ "cw-elevator-application-2.0.6.jar"
)
allow_b = args.allow_baseline_only
if allow_b is None:
p = root / "docs" / "testing" / "cw-elevator-fatjar-lib-parity-allow-baseline-only.txt"
allow_b = p if p.is_file() else None
allow_c = args.allow_candidate_only
if allow_c is None:
p = root / "docs" / "testing" / "cw-elevator-fatjar-lib-parity-allow-candidate-only.txt"
allow_c = p if p.is_file() else None
return run_lib_gate(b, cand, allow_b, allow_c)
v1_jar = root / "cw-elevator-application-V1.0.0.20211103" / "cw-elevator-application-V1.0.0.20211103.jar"
v2_jar = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.7.jar"
)
if not v2_jar.is_file():
v2_jar = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.0.jar"
)
if not v2_jar.is_file():
v2_jar = (
root
/ "maven-cw-elevator-application"
/ "releases"
/ "cw-elevator-application-V2.0.6.20260430"
/ "cw-elevator-application-2.0.6.jar"
)
# reactor + dependency:list 时各模块写各自 target/starter 模块输出才是入口 fat-jar 的 runtime 列表
mvn_list = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "v2-maven-deps.txt"
)
if not mvn_list.is_file():
mvn_list = root / "maven-cw-elevator-application" / "target" / "v2-maven-deps.txt"
if not mvn_list.is_file():
mvn_list = Path("/tmp/v2-maven-deps.txt")
out_md = (
root
/ "docs"
/ "testing"
/ "cw-elevator-v1-v2-dependency-diff.md"
)
if not v1_jar.is_file():
print("Missing V1 jar:", v1_jar, file=sys.stderr)
return 1
if not v2_jar.is_file():
print("Missing V2 jar:", v2_jar, file=sys.stderr)
return 1
v1_map = list_outer_nested(v1_jar, "lib/")
v2_prefix = detect_lib_prefix(v2_jar)
v2_map = list_outer_nested(v2_jar, v2_prefix)
v1_by_ga: dict[str, list[str]] = defaultdict(list)
for path, t in v1_map.items():
v1_by_ga[key_ga(path, t)].append(path)
v2_by_ga: dict[str, list[str]] = defaultdict(list)
for path, t in v2_map.items():
v2_by_ga[key_ga(path, t)].append(path)
keys1 = set(v1_by_ga.keys())
keys2 = set(v2_by_ga.keys())
only_v1 = sorted(keys1 - keys2)
only_v2 = sorted(keys2 - keys1)
both = sorted(keys1 & keys2)
mvn_rows: list[tuple[str, str, str, str]] = []
if mvn_list.is_file():
mvn_rows = parse_maven_dependency_list(mvn_list)
mvn_ga = {f"{g}:{a}:{v}" for g, a, v, _ in mvn_rows}
lines: list[str] = []
lines.append("# cw-elevator-application V1 fat-jar 与 V2 fat-jar 依赖差异核对")
lines.append("")
lines.append("**生成方式**:脚本 `scripts/generate_v1_v2_elevator_dependency_diff.py`(可重复执行覆盖本文件)。")
lines.append("")
lines.append("## 样本路径")
lines.append("")
lines.append(f"- **V1**`{v1_jar.relative_to(root)}`")
lines.append(f"- **V2**`{v2_jar.relative_to(root)}`")
lines.append("")
lines.append("| 指标 | V1 | V2 |")
lines.append("|------|----|----|")
lines.append(f"| 嵌套 jar 条目数(lib / BOOT-INF/lib | {len(v1_map)} | {len(v2_map)} |")
lines.append(f"| 解析出唯一坐标 `groupId:artifactId:version` 数 | {len(keys1)} | {len(keys2)} |")
v1_ga_v = collect_ga_versions_from_jar_map(v1_map)
v2_ga_v = collect_ga_versions_from_jar_map(v2_map)
skew_ga = ga_version_skew_rows(v1_ga_v, v2_ga_v)
lines.append(
f"| 同名 GA、两侧均有解析且 version 集合不一致(§2.2.1)条数 | — | **{len(skew_ga)}** |"
)
lines.append(
f"| 与 Maven `dependency:list`runtime)条目数 | — | {len(mvn_rows)} |"
)
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 1. Maven 方式(仅 V2 reactor")
lines.append("")
lines.append(
"在 `maven-cw-elevator-application` 下执行:`mvn -pl cw-elevator-application-starter -am "
"dependency:list -DincludeScope=runtime -Dsort=true "
"-DoutputFile=target/v2-maven-deps.txt`。`-am` 时每个子模块写各自的 `target/`;"
"**§1.1 使用 starter 模块文件**`cw-elevator-application-starter/target/v2-maven-deps.txt`。"
)
lines.append("")
lines.append(
"**说明**:历史 **V1 运行包** 当前仓库无对应 **1.0** 聚合工程可一键 `dependency:list`"
"V1 的 Maven 坐标视图见 **§2 二进制嵌套 JAR 的 pom.properties**。"
)
lines.append("")
lines.append("### 1.1 V2 `dependency:list` 全量(runtime")
lines.append("")
lines.append("| # | groupId | artifactId | version | scope |")
lines.append("|---|---------|--------------|---------|-------|")
for i, (g, a, v, s) in enumerate(mvn_rows, 1):
lines.append(f"| {i} | `{g}` | `{a}` | `{v}` | `{s}` |")
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 2. 二进制方式(嵌套 JAR + pom.properties")
lines.append("")
lines.append(
f"- **V1**`lib/*.jar`。\n"
f"- **V2**:自动检测为 `{v2_prefix}*.jar`(与 spring-boot-maven-plugin 1.3.x + Boot 1.5 一致时为 `lib/`)。"
)
lines.append("")
lines.append(
"对每个嵌套 jar 读取 `META-INF/maven/**/pom.properties` 得到 `groupId:artifactId:version`"
"无法读取时记为 `?:?:?`(多为无 Maven 元数据的第三方包)。"
)
lines.append("")
lines.append("### 2.1 仅在 V1 出现的坐标(相对 V2 二进制集合)")
lines.append("")
lines.append(f"**共 {len(only_v1)} 项**。")
lines.append("")
lines.append("| groupId:artifactId:version | V1 嵌套路径 |")
lines.append("|------------------------------|-------------|")
for k in only_v1:
paths = ", ".join(f"`{p}`" for p in sorted(v1_by_ga[k]))
lines.append(f"| `{k}` | {paths} |")
lines.append("")
lines.append("### 2.2 仅在 V2 出现的坐标(相对 V1 二进制集合)")
lines.append("")
lines.append(f"**共 {len(only_v2)} 项**。")
lines.append("")
lines.append("| groupId:artifactId:version | V2 嵌套路径 |")
lines.append("|------------------------------|-------------|")
for k in only_v2:
paths = ", ".join(f"`{p}`" for p in sorted(v2_by_ga[k]))
lines.append(f"| `{k}` | {paths} |")
lines.append("")
lines.append("### 2.2.1 同名构件(groupId:artifactId)在 V1 与 V2 中的版本集合差异")
lines.append("")
lines.append(
"由嵌套 jar 的 `pom.properties` 聚合:若同一 **GA** 在 V1、V2 中均能解析出版本,且 **version 集合不同**"
"则单独列出(与 §2.1 / §2.2 中分列的 `g:a:v` 键互为补充)。**不含**仅一侧出现的 GA。"
)
lines.append("")
lines.append(f"**共 {len(skew_ga)} 项**。")
lines.append("")
lines.append("| groupId:artifactId | V1 version(s) | V2 version(s) |")
lines.append("|--------------------|---------------|---------------|")
for (g, a), v1s, v2s in skew_ga:
ga_s = f"`{g}:{a}`"
lines.append(
f"| {ga_s} | `{', '.join(v1s)}` | `{', '.join(v2s)}` |"
)
lines.append("")
lines.append("### 2.3 两边均存在且坐标一致的依赖")
lines.append("")
lines.append(f"**共 {len(both)} 项**(名称版本完全一致)。")
lines.append("")
lines.append("<details>")
lines.append("<summary>展开长表</summary>")
lines.append("")
lines.append("| groupId:artifactId:version |")
lines.append("|------------------------------|")
for k in both:
lines.append(f"| `{k}` |")
lines.append("")
lines.append("</details>")
lines.append("")
lines.append("### 2.4 V2 二进制坐标 vs Maven dependency:list")
lines.append("")
lines.append(
"- **版本字符串不一致**:例如 reactor 在 `dependency:list` 中为 **`2.0-SNAPSHOT`**"
"而 fat-jar 内嵌模块 **`cw-elevator-application-*-2.0.6.jar`** 的 `pom.properties` 为 **`2.0.6`**"
"字符串比对会视为「仅一侧存在」,属**同名构件不同表述**,非缺失依赖。"
)
lines.append(
"- **在 dependency:list 中但不在嵌套 jar 元数据中的**:多为 **仅存在于解析树、与本模块 jar 文件命名不一致**,需对照 §1 表格。"
)
lines.append(
"- **未解析 `unresolved:*`**:见 §3,此类条目不参与坐标相等判断。"
)
lines.append("")
only_mvn = sorted(mvn_ga - keys2)
only_bin = sorted(keys2 - mvn_ga)
lines.append(f"- **仅在 Maven listruntime**{len(only_mvn)}")
lines.append("")
if only_mvn:
lines.append("|坐标|")
lines.append("|----|")
for k in only_mvn[:80]:
lines.append(f"| `{k}` |")
if len(only_mvn) > 80:
lines.append(f"| … 其余 {len(only_mvn) - 80} 项省略 |")
lines.append("")
lines.append(f"- **仅在二进制坐标集合**{len(only_bin)}")
lines.append("")
if only_bin:
lines.append("|坐标|")
lines.append("|----|")
for k in only_bin[:80]:
lines.append(f"| `{k}` |")
if len(only_bin) > 80:
lines.append(f"| … 其余 {len(only_bin) - 80} 项省略 |")
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 3. 无法解析 pom.properties 的嵌套 JAR(仅列文件名)")
lines.append("")
bad_v1 = [p for p, t in v1_map.items() if t[0] == "?" or t[2] == "?"]
bad_v2 = [p for p, t in v2_map.items() if t[0] == "?" or t[2] == "?"]
lines.append(f"- **V1** 未解析条目:**{len(bad_v1)}**")
for p in sorted(bad_v1)[:50]:
lines.append(f" - `{Path(p).name}`")
if len(bad_v1) > 50:
lines.append(f" - … 省略 {len(bad_v1) - 50}")
lines.append(f"- **V2** 未解析条目:**{len(bad_v2)}**")
for p in sorted(bad_v2)[:50]:
lines.append(f" - `{Path(p).name}`")
if len(bad_v2) > 50:
lines.append(f" - … 省略 {len(bad_v2) - 50}")
lines.append("")
out_md.parent.mkdir(parents=True, exist_ok=True)
out_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
print("Wrote", out_md)
return 0
if __name__ == "__main__":
sys.exit(main())
+116 -20
View File
@@ -1,14 +1,19 @@
#!/usr/bin/env bash
# 构建 cw-elevator-application 指定版本发布包,输出到 maven 模块下 releases/<version>/
# 构建 cw-elevator-application 指定版本发布包,输出到 maven 模块下 releases/。
# 目录命名对齐历史运行包 cw-elevator-application-V1.0.0.20211103
# cw-elevator-application-V<版本>.<日期>(日期默认当天 YYYYMMDD,可用 RELEASE_DATE_LABEL 覆盖)。
# 用法:在仓库根执行 ./scripts/release-cw-elevator-application.sh [版本号]
# 默认版本与根 POM 中 elevator.release.finalName 后缀一致(当前 2.0.0)。
# 默认版本与根 POM 中 elevator.release.finalName 后缀一致(当前 2.0.8)。
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
MAVEN_ROOT="${ROOT}/maven-cw-elevator-application"
REL_VER="${1:-2.0.0}"
REL_VER="${1:-2.0.8}"
JAR_NAME="cw-elevator-application-${REL_VER}.jar"
OUT_DIR="${MAVEN_ROOT}/releases/v${REL_VER}"
RELEASE_DATE_LABEL="${RELEASE_DATE_LABEL:-$(date +%Y%m%d)}"
BUNDLE_DIR_NAME="cw-elevator-application-V${REL_VER}.${RELEASE_DATE_LABEL}"
OUT_DIR="${MAVEN_ROOT}/releases/${BUNDLE_DIR_NAME}"
DOC_FALLBACK_VER="${DOC_FALLBACK_VER:-2.0.6}"
JAVA_HOME="${JAVA_HOME:-/usr/lib/jvm/java-8-openjdk-amd64}"
export JAVA_HOME
@@ -20,8 +25,18 @@ if ! java -version 2>&1 | grep -q 'version "1\.8\.'; then
exit 1
fi
rm -rf "${OUT_DIR}"
mkdir -p "${OUT_DIR}"
require_file() {
local file_path="$1"
local hint="$2"
if [[ ! -f "${file_path}" ]]; then
echo "ERROR: 缺少必需文件: ${file_path} (${hint})" >&2
exit 1
fi
}
echo "==> Set reactor version to ${REL_VER}"
(cd "${MAVEN_ROOT}" && mvn -q org.codehaus.mojo:versions-maven-plugin:2.16.2:set \
-DnewVersion="${REL_VER}" -DprocessAllModules=true -DgenerateBackupPoms=false)
@@ -31,7 +46,16 @@ echo "==> Package starter (fat jar)"
SRC_JAR="${MAVEN_ROOT}/cw-elevator-application-starter/target/${JAR_NAME}"
if [[ ! -f "${SRC_JAR}" ]]; then
echo "ERROR: 未找到 ${SRC_JAR}" >&2
for candidate in $(ls -1t "${MAVEN_ROOT}/cw-elevator-application-starter/target"/cw-elevator-application-*.jar 2>/dev/null || true); do
if [[ "${candidate}" == *.jar.original ]]; then
continue
fi
SRC_JAR="${candidate}"
break
done
fi
if [[ -z "${SRC_JAR}" || ! -f "${SRC_JAR}" ]]; then
echo "ERROR: 未找到可用 starter 制品(期望 ${JAR_NAME}" >&2
exit 1
fi
@@ -41,36 +65,100 @@ install -m0644 "${SRC_JAR}" "${OUT_DIR}/${JAR_NAME}"
DDL_SRC="${ROOT}/docs/sql/tenant_visitor_floor_policy.sql"
DDL_DIR="${OUT_DIR}/ddl"
mkdir -p "${DDL_DIR}"
if [[ -f "${DDL_SRC}" ]]; then
install -m0644 "${DDL_SRC}" "${DDL_DIR}/tenant_visitor_floor_policy.sql"
else
echo "WARN: 未找到 ${DDL_SRC},发布包未含 DDL" >&2
fi
require_file "${DDL_SRC}" "DDL"
install -m0644 "${DDL_SRC}" "${DDL_DIR}/tenant_visitor_floor_policy.sql"
DDL_INIT_SRC="${ROOT}/docs/sql/tenant_visitor_floor_policy_init_guangfa_fund.sql"
require_file "${DDL_INIT_SRC}" "初始化 SQL"
install -m0644 "${DDL_INIT_SRC}" "${DDL_DIR}/tenant_visitor_floor_policy_init_guangfa_fund.sql"
UPGRADE_SRC="${ROOT}/docs/build/cw-elevator-application-v${REL_VER}-版本升级说明书.md"
if [[ -f "${UPGRADE_SRC}" ]]; then
install -m0644 "${UPGRADE_SRC}" "${OUT_DIR}/版本升级说明书.md"
else
echo "WARN: 未找到 ${UPGRADE_SRC},跳过 版本升级说明书.md" >&2
if [[ ! -f "${UPGRADE_SRC}" ]]; then
UPGRADE_SRC="${ROOT}/docs/build/cw-elevator-application-v${DOC_FALLBACK_VER}-版本升级说明书.md"
fi
require_file "${UPGRADE_SRC}" "版本升级说明书"
install -m0644 "${UPGRADE_SRC}" "${OUT_DIR}/版本升级说明书.md"
INDEX_SRC="${ROOT}/docs/build/cw-elevator-application-v${REL_VER}-发布说明.md"
if [[ -f "${INDEX_SRC}" ]]; then
install -m0644 "${INDEX_SRC}" "${OUT_DIR}/发布说明.md"
if [[ ! -f "${INDEX_SRC}" ]]; then
INDEX_SRC="${ROOT}/docs/build/cw-elevator-application-v${DOC_FALLBACK_VER}-发布说明.md"
fi
require_file "${INDEX_SRC}" "发布说明"
install -m0644 "${INDEX_SRC}" "${OUT_DIR}/发布说明.md"
CLIENT_SRC="${ROOT}/docs/build/cw-elevator-application-v${REL_VER}-甲方版本升级说明.md"
if [[ -f "${CLIENT_SRC}" ]]; then
install -m0644 "${CLIENT_SRC}" "${OUT_DIR}/甲方版本升级说明.md"
if [[ ! -f "${CLIENT_SRC}" ]]; then
CLIENT_SRC="${ROOT}/docs/build/cw-elevator-application-v${DOC_FALLBACK_VER}-甲方版本升级说明.md"
fi
require_file "${CLIENT_SRC}" "甲方版本升级说明"
install -m0644 "${CLIENT_SRC}" "${OUT_DIR}/甲方版本升级说明.md"
PLAN_SRC="${ROOT}/docs/build/cw-elevator-application-v${REL_VER}-升级计划.md"
if [[ -f "${PLAN_SRC}" ]]; then
install -m0644 "${PLAN_SRC}" "${OUT_DIR}/升级计划.md"
if [[ ! -f "${PLAN_SRC}" ]]; then
PLAN_SRC="${ROOT}/docs/build/cw-elevator-application-v${DOC_FALLBACK_VER}-升级计划.md"
fi
require_file "${PLAN_SRC}" "升级计划"
install -m0644 "${PLAN_SRC}" "${OUT_DIR}/升级计划.md"
# 与历史运行包 cw-elevator-application-V1.0.0.20211103 一致:bootstrap/application*.properties 仅置于发布根目录(与 JAR、start.sh 同层),不另建 config/ 重复一份。
for conf_name in bootstrap.properties application.properties application-access-control.properties; do
CONF_SRC="${MAVEN_ROOT}/deploy/v2-maven/${conf_name}"
require_file "${CONF_SRC}" "配置文件 ${conf_name}"
install -m0644 "${CONF_SRC}" "${OUT_DIR}/${conf_name}"
done
COMMON_JAVA_SRC="${MAVEN_ROOT}/deploy/common-java.sh"
require_file "${COMMON_JAVA_SRC}" "common-java.shrun.sh 依赖)"
install -m0644 "${COMMON_JAVA_SRC}" "${OUT_DIR}/common-java.sh"
RUNNER_SRC="${MAVEN_ROOT}/deploy/v2-maven/run.sh"
require_file "${RUNNER_SRC}" "启动脚本 run.sh"
install -m0755 "${RUNNER_SRC}" "${OUT_DIR}/run.sh"
TEMPLATE_DIR="${MAVEN_ROOT}/deploy/release-bundle-templates"
require_file "${TEMPLATE_DIR}/start.sh.template" "release-bundle start.sh.template"
require_file "${TEMPLATE_DIR}/stop.sh.template" "release-bundle stop.sh.template"
require_file "${TEMPLATE_DIR}/cw-elevator-application.service.template" "release-bundle service template"
DEPLOY_DIR_PLACEHOLDER="${DEPLOY_DIR_PLACEHOLDER:-/path/to/cw-elevator-application}"
sed -e "s/__JAR_NAME__/${JAR_NAME}/g" -e "s/__REL_VER__/${REL_VER}/g" \
"${TEMPLATE_DIR}/start.sh.template" > "${OUT_DIR}/start.sh"
chmod 0755 "${OUT_DIR}/start.sh"
sed -e "s/__JAR_NAME__/${JAR_NAME}/g" \
"${TEMPLATE_DIR}/stop.sh.template" > "${OUT_DIR}/stop.sh"
chmod 0755 "${OUT_DIR}/stop.sh"
sed -e "s/__JAR_NAME__/${JAR_NAME}/g" -e "s/__REL_VER__/${REL_VER}/g" \
-e "s|__DEPLOY_DIR__|${DEPLOY_DIR_PLACEHOLDER}|g" \
"${TEMPLATE_DIR}/cw-elevator-application.service.template" > "${OUT_DIR}/cw-elevator-application.service"
chmod 0644 "${OUT_DIR}/cw-elevator-application.service"
EVIDENCE_SRC="${ROOT}/scripts/collect_elevator_runtime_evidence.sh"
require_file "${EVIDENCE_SRC}" "collect_elevator_runtime_evidence.sh(现场证据采集)"
install -m0755 "${EVIDENCE_SRC}" "${OUT_DIR}/collect_elevator_runtime_evidence.sh"
DELIVERY_SRC="${ROOT}/docs/build/cw-elevator-application-v${REL_VER}-实施交付清单.md"
if [[ ! -f "${DELIVERY_SRC}" ]]; then
DELIVERY_SRC="${ROOT}/docs/build/cw-elevator-application-v${DOC_FALLBACK_VER}-实施交付清单.md"
fi
require_file "${DELIVERY_SRC}" "实施交付清单"
install -m0644 "${DELIVERY_SRC}" "${OUT_DIR}/实施交付清单.md"
ACCEPTANCE_SRC="${ROOT}/docs/build/cw-elevator-application-v${REL_VER}-实施验收记录模板.md"
if [[ ! -f "${ACCEPTANCE_SRC}" ]]; then
ACCEPTANCE_SRC="${ROOT}/docs/build/cw-elevator-application-v${DOC_FALLBACK_VER}-实施验收记录模板.md"
fi
require_file "${ACCEPTANCE_SRC}" "实施验收记录模板"
install -m0644 "${ACCEPTANCE_SRC}" "${OUT_DIR}/实施验收记录模板.md"
AUDIT_SRC="${ROOT}/docs/build/cw-elevator-application-v${REL_VER}-SQL与代码一致性审核记录.md"
if [[ ! -f "${AUDIT_SRC}" ]]; then
AUDIT_SRC="${ROOT}/docs/build/cw-elevator-application-v${DOC_FALLBACK_VER}-SQL与代码一致性审核记录.md"
fi
require_file "${AUDIT_SRC}" "SQL与代码一致性审核记录"
install -m0644 "${AUDIT_SRC}" "${OUT_DIR}/SQL与代码一致性审核记录.md"
{
echo "artifact=${JAR_NAME}"
echo "bundle_dir_name=${BUNDLE_DIR_NAME}"
echo "directory=${OUT_DIR}"
echo "built_at=$(date -Iseconds 2>/dev/null || date)"
echo "java_home=${JAVA_HOME}"
@@ -81,6 +169,14 @@ fi
(cd "${ROOT}" && git rev-parse --abbrev-ref HEAD 2>/dev/null) || echo "unknown"
} > "${OUT_DIR}/BUILD_MANIFEST.txt"
if [[ "${RELEASE_MAKE_ZIP:-1}" == "1" ]]; then
ZIP_NAME="${BUNDLE_DIR_NAME}.zip"
ZIP_OUT="${MAVEN_ROOT}/releases/${ZIP_NAME}"
rm -f "${ZIP_OUT}"
(cd "${MAVEN_ROOT}/releases" && zip -rq "${ZIP_NAME}" "${BUNDLE_DIR_NAME}")
echo "==> Zip artifact: ${ZIP_OUT}"
fi
echo "==> Restore reactor version to 2.0-SNAPSHOT"
(cd "${MAVEN_ROOT}" && mvn -q org.codehaus.mojo:versions-maven-plugin:2.16.2:set \
-DnewVersion=2.0-SNAPSHOT -DprocessAllModules=true -DgenerateBackupPoms=false)
+252
View File
@@ -0,0 +1,252 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
前端运行时自动验收(Playwright):
1) 打开页面并等待首屏稳定
2) 采集 console/pageerror/requestfailed
3) 统计重复资源请求(按 URL 去 query 后聚合)
4) 输出 markdown 报告 + 截图
"""
from __future__ import annotations
import argparse
import asyncio
import datetime as dt
import json
import re
import sys
from collections import Counter
from pathlib import Path
from typing import Dict, List, Tuple
from urllib.parse import urlsplit, urlunsplit
from playwright.async_api import Error as PlaywrightError
from playwright.async_api import async_playwright
def _strip_query(url: str) -> str:
p = urlsplit(url)
return urlunsplit((p.scheme, p.netloc, p.path, "", ""))
def _now_tag() -> str:
return dt.datetime.now().strftime("%Y%m%d-%H%M%S")
def _classify_resource(url: str) -> str:
path = urlsplit(url).path.lower()
if path.endswith(".js"):
return "js"
if path.endswith(".css"):
return "css"
if re.search(r"\.(png|jpg|jpeg|gif|svg|ico|webp)$", path):
return "image"
if "/api/" in path or "/elevator/" in path:
return "api"
return "other"
async def run_check(url: str, wait_ms: int, screenshot_path: Path) -> Dict[str, object]:
console_errors: List[str] = []
console_warnings: List[str] = []
page_errors: List[str] = []
failed_requests: List[str] = []
requests: List[Tuple[str, str]] = []
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(ignore_https_errors=True)
page = await context.new_page()
page.set_default_timeout(5_000)
page.on(
"console",
lambda msg: (
console_errors.append(msg.text)
if msg.type == "error"
else console_warnings.append(msg.text)
if msg.type in {"warning", "warn"}
else None
),
)
page.on("pageerror", lambda err: page_errors.append(str(err)))
def _on_request_failed(req) -> None:
failure = req.failure
if isinstance(failure, dict):
err_text = failure.get("errorText", "unknown")
elif isinstance(failure, str):
err_text = failure
elif failure is None:
err_text = "unknown"
else:
err_text = getattr(failure, "error_text", str(failure))
failed_requests.append(f"{req.url} | {err_text}")
page.on("requestfailed", _on_request_failed)
page.on("requestfinished", lambda req: requests.append((req.method, req.url)))
navigation_mode = "domcontentloaded"
try:
try:
await page.goto(url, wait_until="domcontentloaded", timeout=12_000)
except PlaywrightError:
# 某些历史页面会因脚本阻塞导致 domcontentloaded 不触发,降级到 commit 保证可诊断
navigation_mode = "commit(fallback)"
await page.goto(url, wait_until="commit", timeout=20_000)
await page.wait_for_timeout(wait_ms)
try:
# 避免 full_page 在超长文档上触发超时,先抓取可视区域。
await page.screenshot(path=str(screenshot_path), full_page=False, timeout=15_000)
except Exception:
screenshot_path.write_bytes(b"")
try:
title = await page.title()
except Exception:
title = ""
try:
body_non_empty = bool(await page.evaluate("Boolean(document.body && document.body.innerText && document.body.innerText.trim())"))
except Exception:
body_non_empty = False
try:
app_count = await page.locator("#app *").count()
except Exception:
app_count = -1
except PlaywrightError as e:
await browser.close()
return {
"fatal_error": str(e),
"console_errors": console_errors,
"console_warnings": console_warnings,
"page_errors": page_errors,
"failed_requests": failed_requests,
"requests": requests,
}
await browser.close()
normalized_urls = [_strip_query(u) for _, u in requests]
url_counter = Counter(normalized_urls)
duplicate_urls = [(u, c) for u, c in url_counter.items() if c > 1]
duplicate_urls.sort(key=lambda x: x[1], reverse=True)
resource_counter = Counter(_classify_resource(u) for u in normalized_urls)
return {
"fatal_error": "",
"navigation_mode": navigation_mode,
"title": title,
"body_non_empty": body_non_empty,
"app_count": app_count,
"console_errors": console_errors,
"console_warnings": console_warnings,
"page_errors": page_errors,
"failed_requests": failed_requests,
"requests_total": len(requests),
"requests_unique": len(url_counter),
"resource_counter": dict(resource_counter),
"duplicate_urls": duplicate_urls[:30],
}
def _render_report(url: str, result: Dict[str, object], screenshot_path: Path) -> str:
lines: List[str] = []
lines.append("# 前端运行时自动验收报告")
lines.append("")
lines.append(f"- URL: `{url}`")
lines.append(f"- 时间: `{dt.datetime.now().isoformat(timespec='seconds')}`")
lines.append(f"- 截图: `{screenshot_path}`")
lines.append("")
fatal = str(result.get("fatal_error", "")).strip()
if fatal:
lines.append("## 结论")
lines.append("- **失败**:页面打开阶段异常")
lines.append(f"- 异常: `{fatal}`")
return "\n".join(lines) + "\n"
lines.append("## 页面状态")
lines.append(f"- 导航模式: `{result.get('navigation_mode', '')}`")
lines.append(f"- title: `{result.get('title', '')}`")
lines.append(f"- body 是否非空: `{result.get('body_non_empty', False)}`")
lines.append(f"- `#app` 子节点数: `{result.get('app_count', 0)}`")
lines.append("")
lines.append("## 请求统计")
lines.append(f"- 总请求数: `{result.get('requests_total', 0)}`")
lines.append(f"- 去重后 URL 数: `{result.get('requests_unique', 0)}`")
lines.append(f"- 类型分布: `{json.dumps(result.get('resource_counter', {}), ensure_ascii=False)}`")
lines.append("")
dup = result.get("duplicate_urls", [])
lines.append("## 重复请求(Top")
if dup:
for u, c in dup:
lines.append(f"- `{c}x` {u}")
else:
lines.append("- 无重复 URL 请求")
lines.append("")
def _section(title: str, rows: List[str]) -> None:
lines.append(f"## {title}")
if rows:
for row in rows[:50]:
lines.append(f"- {row}")
else:
lines.append("- 无")
lines.append("")
_section("Console Error", result.get("console_errors", []))
_section("Page Error", result.get("page_errors", []))
_section("Request Failed", result.get("failed_requests", []))
_section("Console Warning", result.get("console_warnings", []))
return "\n".join(lines)
async def _amain() -> int:
parser = argparse.ArgumentParser(description="自动检测前端运行时错误/重复加载")
parser.add_argument("--url", default="http://127.0.0.1:8090/login", help="待检测 URL")
parser.add_argument(
"--wait-ms", type=int, default=5000, help="页面打开后额外等待毫秒数(默认 5000)"
)
parser.add_argument(
"--out-dir",
default="artifacts/frontend-check",
help="报告与截图输出目录(默认 artifacts/frontend-check",
)
args = parser.parse_args()
out_dir = Path(args.out_dir).resolve()
out_dir.mkdir(parents=True, exist_ok=True)
tag = _now_tag()
screenshot = out_dir / f"frontend-{tag}.png"
report = out_dir / f"frontend-{tag}.md"
result = await run_check(args.url, args.wait_ms, screenshot)
report.write_text(_render_report(args.url, result, screenshot), encoding="utf-8")
print(f"[frontend-check] report={report}")
print(f"[frontend-check] screenshot={screenshot}")
if result.get("fatal_error"):
return 2
if result.get("console_errors") or result.get("page_errors") or result.get("failed_requests"):
return 1
return 0
def main() -> int:
try:
return asyncio.run(_amain())
except KeyboardInterrupt:
return 130
except Exception as e:
print(f"[frontend-check] unexpected error: {e}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
V1_DIR="${ROOT_DIR}/cw-elevator-application-V1.0.0.20211103"
V2_DIR="${ROOT_DIR}/maven-cw-elevator-application/deploy/v2-maven"
V2_RELEASES_DIR="${ROOT_DIR}/maven-cw-elevator-application/releases"
V1_JAR="${V1_DIR}/cw-elevator-application-V1.0.0.20211103.jar"
V2_JAR=""
for candidate in $(ls -1t "${V2_RELEASES_DIR}"/v*/cw-elevator-application-*.jar 2>/dev/null || true); do
if [[ -f "${candidate}" ]]; then
V2_JAR="${candidate}"
break
fi
done
if [[ -z "${V2_JAR}" ]]; then
for candidate in $(ls -1t "${V2_DIR}"/cw-elevator-application-*.jar 2>/dev/null || true); do
if [[ -f "${candidate}" ]]; then
V2_JAR="${candidate}"
break
fi
done
fi
if [[ ! -f "${V1_JAR}" ]]; then
echo "ERROR: V1 jar not found: ${V1_JAR}" >&2
exit 1
fi
if [[ -z "${V2_JAR}" || ! -f "${V2_JAR}" ]]; then
echo "ERROR: V2 jar not found under ${V2_DIR}" >&2
exit 1
fi
python3 - "$V1_DIR" "$V2_DIR" "$V1_JAR" "$V2_JAR" <<'PY'
import subprocess
import sys
from pathlib import Path
v1_dir = Path(sys.argv[1])
v2_dir = Path(sys.argv[2])
v1_jar = Path(sys.argv[3])
v2_jar = Path(sys.argv[4])
files = [
"bootstrap.properties",
"application.properties",
"application-access-control.properties",
]
keys = [
"spring.cloud.consul.host",
"spring.cloud.consul.port",
"spring.cloud.consul.discovery.enabled",
"spring.cloud.consul.config.enabled",
"feign.cwos-portal.name",
"feign.ninca-common.name",
"feign.component-organization.name",
]
def parse_properties(text):
result = {}
for raw_line in text.splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or line.startswith("!"):
continue
if "=" in line:
k, v = line.split("=", 1)
elif ":" in line:
k, v = line.split(":", 1)
else:
continue
result[k.strip()] = v.strip()
return result
def read_external(dir_path, name):
p = dir_path / name
if not p.exists():
return None
return parse_properties(p.read_text(encoding="utf-8", errors="ignore"))
def read_jar(jar_path, name):
try:
out = subprocess.check_output(
["jar", "tf", str(jar_path)],
text=True,
errors="ignore",
).splitlines()
except subprocess.CalledProcessError:
return None
candidates = []
suffix = "/" + name
for item in out:
if item == name or item.endswith(suffix):
candidates.append(item)
if not candidates:
return None
preferred = None
for c in candidates:
if c.startswith("BOOT-INF/classes/"):
preferred = c
break
if preferred is None:
preferred = candidates[0]
try:
content = subprocess.check_output(
["unzip", "-p", str(jar_path), preferred],
text=True,
errors="ignore",
)
except subprocess.CalledProcessError:
return None
return parse_properties(content)
def describe_source(external_map, internal_map, key):
ext_val = None if external_map is None else external_map.get(key)
int_val = None if internal_map is None else internal_map.get(key)
if ext_val is not None:
return "external", ext_val
if int_val is not None:
return "jar-internal", int_val
return "unset", ""
def report_version(tag, base_dir, jar_path):
print(f"\n==== {tag} ====")
print(f"base_dir: {base_dir}")
print(f"jar: {jar_path}")
ext_maps = {name: read_external(base_dir, name) for name in files}
int_maps = {name: read_jar(jar_path, name) for name in files}
print("\n[1] 配置文件存在性")
for name in files:
ext_exists = ext_maps[name] is not None
int_exists = int_maps[name] is not None
print(f"- {name}: external={'Y' if ext_exists else 'N'}, jar={'Y' if int_exists else 'N'}")
print("\n[2] 关键键值最终命中来源(按常见优先级:external > jar")
merged_ext = {}
merged_int = {}
for name in files:
if ext_maps[name]:
merged_ext.update(ext_maps[name])
if int_maps[name]:
merged_int.update(int_maps[name])
for key in keys:
src, val = describe_source(merged_ext, merged_int, key)
print(f"- {key}: source={src}, value={val}")
print("\n[3] 加载顺序结论")
has_any_internal = any(int_maps[name] is not None for name in files)
if has_any_internal:
print("- 该版本具备 jar 内置配置兜底。")
else:
print("- 该版本无 jar 内置配置兜底,依赖 external/远端配置。")
print("- 在未显式传 --spring.config.location 时,外部同名配置通常优先于 jar 内置。")
report_version("V1", v1_dir, v1_jar)
report_version("V2", v2_dir, v2_jar)
print("\n==== 总结对比 ====")
print("- V1: external + jar 内置(有兜底)")
v2_internal = any(read_jar(v2_jar, name) is not None for name in files)
if v2_internal:
print("- V2: external + jar 内置(有兜底)")
else:
print("- V2: external 优先,当前样本无 jar 内置同名配置(无兜底)")
print("- 若 external 与远端配置中心并存,最终以运行时属性源优先级为准。")
PY