# -*- coding: utf-8 -*- """ 用 `test` profile 真起一次 —— **CI 用的就是这个 profile**。 ## 为什么单独一个检查 `application-test.yml` 是流水线里 `e2e` 阶段真正加载的配置,而它是"新写的配置"。 **新写的配置从没跑起来过**,正是流水线第一次上线时最典型的红法 —— 而且往往红在一个拼错的属性名上,排查半天。 所以这里用真实的环境变量起一次,确认三件事: ① 能起来;② 迁移跑得过;③ `/actuator/health` 可用。 ## 它不验什么 不验任何业务 —— 业务由 `scripts/e2e-*.py` 打真实接口验。 这里只回答一个问题:**"CI 那套配置能不能把服务拉起来"**。 跑法:python scripts/check-test-profile.py (前提:本地 PG/Redis 在跑、已 `mvn package`) """ import base64 import os import re import socket import subprocess import sys import time import urllib.request sys.stdout.reconfigure(encoding="utf-8") ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) RSA_KEY = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCeHytLx+201AZKhds9B+3LWoVYDJisyimtysGpGip6JLT3iiB2tpK2QL4OAopI/8gaY1iyoxmxU5fUEWa+mPspszciNot4bAdDRZMiCkGOj0zEKJRrTDctnvLVyB6lSd89FX8yD/LS131Big57u9Sk5BYCMO9ZhgCrN1p1qbf/FoJ16zZ3NWSLtGdqVUx2BQjnSRp4Rzf8fq/49MWSXnBmsVm543B85mZMARp24rZ5SwpMwhtrAfVihwqkfaWAfu51oiIwjf0Q8s/wsfth60WoWipG56onwuPtSECKW/Weg5SUQiwoj95iWae+yQFGXRy7hkjk3FhmQArhoK3jiJbrAgMBAAECggEAA3AoD9jECWeGz34Dm1sdx4srVRPJChDHQTkkZtnjzI4ZxTwiopIrtHzJ5SIXAalWH4TDe/bh6yInLfxG4KQQ6gI0k7Dd7Xtj2QmdPmM8s63X/Hfpaj/Y/0vyecWFLRDjIr/h6SPBouX+mjs3P2eBIyi2dcu8mJS6VP66H87V6lz5LyPraFC3ocXTMCabJh+i/Xy4EqV26Mhv+kSrE0CLB2ngfQW98+RgPo5Ndk/A826eGVA16tKsylcTfq3SB/yMSy1xHv/p/IF+PK+Twl3GmJB2jf/dzZpteJIZrPRz+AJ9VJQa35/Chre57JFcA50Avkf5iySzzVfYZDXd0qk6GQKBgQDMD3opopECI5lo21p9+KUZ6poW8iO/Yp2FNYBldMnk+oHcCL6H92dGIZNu0mvcumjEstKH4X9OoI5qV2FFMUrFc6E7NfXWNKOj+eJtLn3WzNRJp6n4x8W8AEeAXzHuv8IbSlG8nsG/g49Tdnaxr2y/iWNj1qGnTfaSyhGRRh133wKBgQDGXla5EmtD9TpCpl7/OSWG8qzv4GfRTaVjC+qz2KnfU5cqiFKLK27uxkP9F8Kdh6EkvOltRA8024FeafpLgTjH3fEKHFywwXSVvssp94IZdoyuxrAqMccReE7akS6IImawm8fu9U0bAwi7WShZHaw2hS9ScM/9cATkDRfRm0DydQKBgHtcQ+rVlzom0UAdk2giH5U6SSrj+Q5jDH0SKeurnb3qXwTo2F4UB3/THnDZjlFi86Etxa5VeQPMdiv9ZRVSRjR1wZus9FAphF/F2cdNWs+3BgL88/kss/ICbTdC1UnS7QJYfG89vj0LyGKZdAnY1jI4WThq8VDWBeEZfWNIctobAoGBAJMoZRmCqoSNpZzQI2UfFdHYsaZ6SUoiWfDyZDjrIIoGTvxEiUeo/vCvsLxPJhcLCOZiJqDifo1t0bki0YW15VOQOEKR68lMmpqEId+ON7uAELmPuBx1NQKmx4ipb2Wab6fQyS9iVsMVKyMg7ye9NYZ0ozh7bDDNKzOq6uKaVEIJAoGAbulymgadkLNVksxPW0O0qIlLVfaOzOeDLGi6ZO4ZBt9wyKgbyMcfwjvuylerkLF/zZqLQ79sOd4JO15ssqHK/YVKiON/A7pDDgxAJpOXxGxy9RXpZWhHL1KBjI9Gcq0gyMSnQbiwpB1Wx3qmjNGDynclW1kNFCQKQpW9xTs4NI0=' JAR = os.path.join(ROOT, "backend", "xt-server", "target", "xt-server-1.0.0-SNAPSHOT.jar") env = dict(os.environ) env.update({ # 指到本地已有的库(与 dev 用的是同一个库,只验"能不能起来") "PG_HOST": "localhost", "PG_PORT": "5432", "PG_DB": "filldb", "PG_USER": "fill", "PG_PASSWORD": "fill_dev_pwd", "REDIS_HOST": "localhost", "REDIS_PORT": "16379", "REDIS_PASSWORD": "redis_dev_pwd", # ⚠️ 密钥一律用 base64(os.urandom(n)) 生成 —— 手写字面量连错两次: # 一次是 AES 少了 2 字节,一次是 HMAC 那串根本不是合法 base64 "XT_JWT_SECRET": base64.b64encode(os.urandom(64)).decode(), "XT_AES_KEY": base64.b64encode(os.urandom(32)).decode(), "XT_HMAC_KEY": base64.b64encode(os.urandom(48)).decode(), "XT_RSA_PRIVATE_KEY": RSA_KEY, }) def kill_tree(proc): """ 杀掉整个进程树(Windows 用 taskkill /T)。 ⚠️ 不能用 `proc.kill()`:它只杀直接子进程,而 JVM 可能带着子进程; 更重要的是——**外层命令被超时掐掉时 finally 不一定会跑**, 所以残留会积累。用 `/T` 至少把"能跑到 finally"的那些清干净, 再靠下面的 preclean_port 兜住剩下的。 """ if proc is None or proc.poll() is not None: return try: subprocess.run(["taskkill", "/F", "/T", "/PID", str(proc.pid)], capture_output=True, timeout=20) except Exception: try: proc.kill() except Exception: pass def preclean_port(port): """ 开跑前清掉这个端口上的残留 java。 ⚠️ 为什么必须有它:脚本用的是**随机空闲端口**,所以下一次跑**不会撞端口** —— 残留的 JVM 于是静静地占着内存与 CPU,只在某次"起 jar 超时"时才隐约暴露。 不清端口 = 残留永远不会被发现。 """ try: r = subprocess.run( ["powershell", "-NoProfile", "-Command", "(Get-NetTCPConnection -State Listen -LocalPort %d -ErrorAction SilentlyContinue)" ".OwningProcess" % port], capture_output=True, text=True, timeout=30) for line in (r.stdout or "").split(): pid = line.strip() if pid.isdigit(): subprocess.run(["taskkill", "/F", "/T", "/PID", pid], capture_output=True, timeout=20) print(" ⚠️ 端口 %d 上有残留进程 %s,已清掉" % (port, pid)) except Exception: pass def free_port() -> int: """ 要一个空闲端口。 ⚠️ 不要写死端口:写死的话**第二次跑就撞端口**,而报的错 (`Port xxx was already in use`)跟"配置对不对"毫无关系 —— 一个只能跑一次的检查等于没有检查。 """ with socket.socket() as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] PORT = free_port() print("用端口 %d(每次随机取,避免重复跑时撞端口)" % PORT) preclean_port(PORT) p = subprocess.Popen( ["java", "-jar", JAR, "--spring.profiles.active=test", "--server.port=%d" % PORT], cwd=os.path.join(ROOT, "backend"), env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace") ok = False reason = "" body = "" try: # ⚠️ 就绪判据是 **HTTP 探活**,不是"日志里出现了某一行": # 子进程 stdout 在管道里是**块缓冲**的,启动日志常常不足一个缓冲区, # 于是一行都读不到;而且 readline() 一旦阻塞,超时的判断就轮不到。 # 实测:单独跑能过、紧跟在几个 JVM 之后跑就失败 —— 时好时坏正是它的特征。 deadline = time.time() + 180 while time.time() < deadline: if p.poll() is not None: reason = "进程自己退了(退出码 %s)" % p.returncode break try: with urllib.request.urlopen( "http://localhost:%d/actuator/health" % PORT, timeout=3) as r: body = r.read().decode() ok = True break except Exception as e: # noqa: BLE001 reason = str(e) time.sleep(1) if not ok and not reason: reason = "180 秒内没探活成功" finally: # ⚠️ 必须杀**进程树**(Windows 用 taskkill /T):JVM 可能带子进程, # 而 p.kill() 只杀直接子进程。**失败的运行恰恰是最需要清理的那些。** kill_tree(p) try: p.wait(timeout=15) except Exception: pass if ok: print("健康检查:", body[:200]) print("\n结果:test profile **能起来**") else: print("\n结果:test profile **起不来**") print(" 原因:" + reason) try: text = (p.stdout.read() or "") if p.stdout else "" except Exception: text = "" if "already in use" in text or ("Port" in text and "in use" in text): print(" ⚠️ 端口被占 —— 这是**环境问题**,不是配置问题") for kw in ("ERROR", "Caused by", "Failed", "migration", "Flyway", "Cannot"): for ln in text.split("\n"): if kw in ln: print(" " + ln.strip()[:160]) break print("---- 尾部 ----") print(text[-1500:]) sys.stdout.flush() raise SystemExit(0 if ok else 1)