#!/usr/bin/env python3 # -*- coding: utf-8 -*- """pbl_blueprint M1a 自查门禁脚本。 用法(机构工作空间根执行):: python3 modules/pbl_blueprint/scripts/selfcheck.py echo "rc=$?" 退出码约定(**本轮修复项 B**): 0 —— 全部检查通过(或仅 SKIP,无 FAIL) 1 —— 存在 FAIL 项(门禁不通过,QC/CI 据此判定) 2 —— 脚本自身无法运行(仓库结构缺失等致命错误) 检查分组: A. 目录结构 —— 包目录/init.py/pyproject.toml/scripts/skill 真实存在 B. 表定义 —— models/*.json 11 个,四段式齐备、primary=["id"]、id str32、 tenant_id 为首业务字段、金额 double(18,2) C. CRUD 契约 —— json/*.json 11 个,根键 tblname+params、editable 三个 .dspy URL、 browserfields 非空、无自创 table/list 格式 D. 代码质量 —— py_compile 全量、禁硬编码库名、sqlor API 白名单 E. 注册同步 —— 函数三处注册(定义 / __init__ 导出 / init.py env 注册) F. 契约运行 —— import pbl_blueprint 与 errors.py 14 符号 G. 租户 fail-closed —— 缺 tenant_id 一律拒绝 H. DDL 生成 —— sql/*.sql 与 models 表集合一致 本脚本**不依赖** sqlor/ahserver/sage 运行时;缺失运行时依赖的项按 SKIP 处理并标注, 不计入 FAIL(避免 CI 环境假失败),但 SKIP 数会在结论中明示。 """ from __future__ import print_function import json import os import re import subprocess import sys # -------------------------------------------------------------------------- # 路径解析:脚本位于 modules/pbl_blueprint/scripts/,仓库根 = 上一级 # -------------------------------------------------------------------------- HERE = os.path.dirname(os.path.abspath(__file__)) REPO_ROOT = os.path.dirname(HERE) # modules/pbl_blueprint PKG_DIR = os.path.join(REPO_ROOT, "pbl_blueprint") # modules/pbl_blueprint/pbl_blueprint MODELS_DIR = os.path.join(PKG_DIR, "models") JSON_DIR = os.path.join(PKG_DIR, "json") SQL_DIR = os.path.join(PKG_DIR, "sql") EXPECTED_TABLES = [ "pbl_blueprint", "pbl_blueprint_node", "pbl_blueprint_edge", "pbl_blueprint_version", "pbl_blueprint_version_delta", "pbl_blueprint_template", "pbl_blueprint_fork", "pbl_blueprint_lock", "pbl_blueprint_publish", "pbl_blueprint_offline", "pbl_blueprint_audit", ] ERROR_SYMBOLS = [ "PblBlueprintError", "ERR_OK", "ERR_TENANT_MISSING", "ERR_PARAM_INVALID", "ERR_NOT_FOUND", "ERR_DUPLICATE", "ERR_STATE_INVALID", "ERR_LOCKED", "ERR_FORBIDDEN", "ERR_DB", "ERR_INTERNAL", "ok", "fail", "err", ] SQLOR_WHITELIST = {"C", "U", "D", "R", "I", "sqlExe"} HARDCODE_DB_PATTERNS = [r"\bDBNAME\s*=", r"dbname\s*=\s*['\"]", r"\bDB_NAME\s*="] # -------------------------------------------------------------------------- # 结果收集 # -------------------------------------------------------------------------- RESULTS = [] # [(status, group, name, detail)] def record(status, group, name, detail=""): RESULTS.append((status, group, name, detail)) mark = {"PASS": "[PASS]", "FAIL": "[FAIL]", "SKIP": "[SKIP]"}.get(status, "[%s]" % status) line = "%s %s | %s" % (mark, group, name) if detail: line += " -> %s" % detail print(line) def check(cond, group, name, detail_ok="", detail_bad=""): record("PASS" if cond else "FAIL", group, name, detail_ok if cond else detail_bad) return bool(cond) def load_json(path): with open(path, "r", encoding="utf-8") as f: return json.load(f) # -------------------------------------------------------------------------- # A. 目录结构 # -------------------------------------------------------------------------- def group_a(): g = "A.目录结构" required = [ (PKG_DIR, "包目录 pbl_blueprint/"), (os.path.join(PKG_DIR, "__init__.py"), "pbl_blueprint/__init__.py"), (os.path.join(PKG_DIR, "init.py"), "pbl_blueprint/init.py"), (os.path.join(PKG_DIR, "errors.py"), "pbl_blueprint/errors.py"), (os.path.join(REPO_ROOT, "pyproject.toml"), "pyproject.toml"), (os.path.join(HERE, "load_path.py"), "scripts/load_path.py"), (os.path.join(REPO_ROOT, "skill", "SKILL.md"), "skill/SKILL.md"), (MODELS_DIR, "pbl_blueprint/models/"), (JSON_DIR, "pbl_blueprint/json/"), (SQL_DIR, "pbl_blueprint/sql/"), ] for path, label in required: check(os.path.exists(path), g, label, "存在 %s" % path, "缺失 %s" % path) # 违禁替代文件 forbidden = ["module.json", "rp.json"] for name in forbidden: bad = os.path.exists(os.path.join(REPO_ROOT, name)) or os.path.exists(os.path.join(PKG_DIR, name)) check(not bad, g, "无违禁替代文件 %s" % name, "未发现", "发现违禁文件 %s" % name) # 模块不得有独立 app.py(模块非部署单元) has_app = os.path.exists(os.path.join(REPO_ROOT, "app.py")) or os.path.exists(os.path.join(PKG_DIR, "app.py")) check(not has_app, g, "模块无独立 app.py", "符合(模块非部署单元)", "发现 app.py,模块不应独立部署") # -------------------------------------------------------------------------- # B. 表定义(models/*.json 四段式) # -------------------------------------------------------------------------- def group_b(): g = "B.表定义" if not os.path.isdir(MODELS_DIR): record("FAIL", g, "models 目录", "缺失 %s" % MODELS_DIR) return files = sorted([f for f in os.listdir(MODELS_DIR) if f.endswith(".json")]) check(len(files) == len(EXPECTED_TABLES), g, "models/*.json 数量 = %d" % len(EXPECTED_TABLES), "实际 %d" % len(files), "实际 %d,期望 %d" % (len(files), len(EXPECTED_TABLES))) names = set(f[:-5] for f in files) missing = [t for t in EXPECTED_TABLES if t not in names] check(not missing, g, "表集合与设计定稿一致", "11 表齐备", "缺失表: %s" % ",".join(missing)) for fn in files: path = os.path.join(MODELS_DIR, fn) tbl = fn[:-5] try: d = load_json(path) except Exception as e: # noqa: BLE001 record("FAIL", g, "%s JSON 可解析" % tbl, "", "解析失败: %s" % e) continue # 四段式 segs = ["summary", "fields", "indexes", "codes"] lack = [s for s in segs if s not in d] check(not lack, g, "%s 四段式齐备" % tbl, "summary/fields/indexes/codes", "缺段: %s" % ",".join(lack)) if lack: continue fields = d.get("fields") or {} # primary = ["id"] primary = d.get("primary") if primary is None: primary = (d.get("summary") or {}).get("primary") check(primary == ["id"], g, "%s primary=[\"id\"]" % tbl, "primary=%s" % (primary,), "primary=%s,期望 ['id']" % (primary,)) # id 为 str32 idf = fields.get("id") or {} idt = str(idf.get("type", "")) check(idt.startswith("str") and "32" in idt, g, "%s id 类型 str32" % tbl, "id.type=%s" % idt, "id.type=%s,期望 str32" % idt) # tenant_id 为首业务字段(id 之后第一个) keys = list(fields.keys()) biz = [k for k in keys if k != "id"] check(bool(biz) and biz[0] == "tenant_id", g, "%s tenant_id 为首业务字段" % tbl, "字段序: id, %s" % (biz[0] if biz else "-"), "首业务字段=%s,期望 tenant_id" % (biz[0] if biz else "-")) # 金额字段 double(18,2) money_bad = [] for k, v in fields.items(): if not isinstance(v, dict): continue t = str(v.get("type", "")) is_money_name = ("amount" in k) or ("money" in k) or ("price" in k) or ("fee" in k) is_money_type = t.startswith("double") or t.startswith("decimal") or t.startswith("money") if is_money_name or is_money_type: if "18" not in t or "2" not in t: money_bad.append("%s:%s" % (k, t)) if money_bad: record("FAIL", g, "%s 金额字段 double(18,2)" % tbl, "", "不合规: %s" % ",".join(money_bad)) else: record("PASS", g, "%s 金额字段 double(18,2)" % tbl, "无金额字段或均合规") # indexes 中 tenant_id 打头(复合索引首列) idx = d.get("indexes") or {} bad_idx = [] if isinstance(idx, dict): for ik, iv in idx.items(): cols = iv.get("fields") if isinstance(iv, dict) else iv if isinstance(cols, str): cols = [c.strip() for c in cols.split(",")] if isinstance(cols, (list, tuple)) and cols and cols[0] != "tenant_id": bad_idx.append("%s%s" % (ik, list(cols))) if bad_idx: record("FAIL", g, "%s 索引 tenant_id 打头" % tbl, "", "首列非 tenant_id: %s" % ";".join(bad_idx)) else: record("PASS", g, "%s 索引 tenant_id 打头" % tbl, "全部复合索引首列为 tenant_id") # -------------------------------------------------------------------------- # C. CRUD 契约(json/*.json) # -------------------------------------------------------------------------- def group_c(): g = "C.CRUD契约" if not os.path.isdir(JSON_DIR): record("FAIL", g, "json 目录", "缺失 %s" % JSON_DIR) return files = sorted([f for f in os.listdir(JSON_DIR) if f.endswith(".json")]) check(len(files) == len(EXPECTED_TABLES), g, "json/*.json 数量 = %d" % len(EXPECTED_TABLES), "实际 %d" % len(files), "实际 %d,期望 %d" % (len(files), len(EXPECTED_TABLES))) for fn in files: path = os.path.join(JSON_DIR, fn) tbl = fn[:-5] try: d = load_json(path) except Exception as e: # noqa: BLE001 record("FAIL", g, "%s JSON 可解析" % tbl, "", "解析失败: %s" % e) continue # 根键仅 tblname + params,无自创 table/list 格式 root = set(d.keys()) check(root == {"tblname", "params"}, g, "%s 根键 = tblname+params" % tbl, "根键=%s" % sorted(root), "根键=%s,期望 ['params','tblname']" % sorted(root)) check("table" not in root and "list" not in root, g, "%s 无自创 table/list 格式" % tbl, "符合 crud-definition-spec", "发现自创键") params = d.get("params") or {} # editable 为三个 .dspy URL ed = params.get("editable") ed_list = ed if isinstance(ed, list) else ([ed] if ed else []) dspy = [u for u in ed_list if isinstance(u, str) and u.endswith(".dspy")] check(len(ed_list) == 3 and len(dspy) == 3, g, "%s editable = 3 个 .dspy URL" % tbl, "editable=%s" % (ed_list,), "editable=%s,期望 3 个 .dspy" % (ed_list,)) # browserfields 非空 bf = params.get("browserfields") bf_nonempty = bool(bf) and (len(bf) > 0 if isinstance(bf, (list, dict, str)) else True) check(bf_nonempty, g, "%s browserfields 非空" % tbl, "%d 项" % (len(bf) if hasattr(bf, "__len__") else 1), "browserfields 为空/缺失") # tblname 与文件名一致 check(d.get("tblname") == tbl, g, "%s tblname 与文件名一致" % tbl, "tblname=%s" % d.get("tblname"), "tblname=%s,文件名=%s" % (d.get("tblname"), tbl)) # -------------------------------------------------------------------------- # D. 代码质量 # -------------------------------------------------------------------------- def iter_py_files(): out = [] for root, dirs, fs in os.walk(REPO_ROOT): dirs[:] = [x for x in dirs if x not in (".git", "__pycache__", ".venv", "build", "dist")] for f in fs: if f.endswith(".py"): out.append(os.path.join(root, f)) return sorted(out) def group_d(): g = "D.代码质量" pys = iter_py_files() check(len(pys) > 0, g, "存在 .py 源文件", "%d 个" % len(pys), "未找到任何 .py") # py_compile 全量 try: r = subprocess.run([sys.executable, "-m", "py_compile"] + pys, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out = (r.stdout or b"").decode("utf-8", "replace") + (r.stderr or b"").decode("utf-8", "replace") check(r.returncode == 0, g, "py_compile 全量编译(%d 文件)" % len(pys), "rc=0", "rc=%s\n%s" % (r.returncode, out.strip()[:2000])) except Exception as e: # noqa: BLE001 record("FAIL", g, "py_compile 全量编译", "", "执行异常: %s" % e) # 禁硬编码库名 hits = [] for p in pys: try: txt = open(p, "r", encoding="utf-8").read() except Exception: # noqa: BLE001 continue for i, line in enumerate(txt.splitlines(), 1): s = line.strip() if s.startswith("#"): continue for pat in HARDCODE_DB_PATTERNS: if re.search(pat, line): hits.append("%s:%d:%s" % (os.path.relpath(p, REPO_ROOT), i, s[:120])) check(not hits, g, "禁硬编码库名(DBNAME=/dbname='…'/DB_NAME=)", "无命中,统一走 ServerEnv().get_module_dbname('pbl_blueprint')", "命中 %d 处:\n %s" % (len(hits), "\n ".join(hits[:20]))) # sqlor API 白名单 used = set() for p in pys: try: txt = open(p, "r", encoding="utf-8").read() except Exception: # noqa: BLE001 continue for m in re.finditer(r"\bsor\.([A-Za-z_][A-Za-z0-9_]*)", txt): used.add(m.group(1)) illegal = sorted(used - SQLOR_WHITELIST) check(not illegal, g, "sqlor API 白名单(仅 C/U/D/R/I/sqlExe)", "实际使用: %s" % (sorted(used) or "无"), "非法 API: %s(禁编造 save/list/insert)" % ",".join(illegal)) # -------------------------------------------------------------------------- # E. 函数三处注册同步 # -------------------------------------------------------------------------- def group_e(): g = "E.注册同步" init_py = os.path.join(PKG_DIR, "__init__.py") loader = os.path.join(PKG_DIR, "init.py") if not (os.path.exists(init_py) and os.path.exists(loader)): record("FAIL", g, "__init__.py / init.py 存在", "", "缺失,无法核验三处注册") return init_txt = open(init_py, "r", encoding="utf-8").read() load_txt = open(loader, "r", encoding="utf-8").read() # errors.py 14 符号在 __init__.py 中导出 lack = [s for s in ERROR_SYMBOLS if s not in init_txt] check(not lack, g, "__init__.py 导出 errors 14 符号", "14 符号齐备", "缺导出: %s" % ",".join(lack)) # load_pbl_blueprint 入口存在 check("def load_pbl_blueprint" in load_txt, g, "init.py 定义 load_pbl_blueprint()", "入口存在", "未找到 load_pbl_blueprint 定义") # get_module_dbname 取库名(禁硬编码) check("get_module_dbname" in load_txt, g, "init.py 用 get_module_dbname 取库名", "符合规范", "未使用 ServerEnv().get_module_dbname") # env 注册(api 函数挂到 ServerEnv) has_reg = bool(re.search(r"(register|set_api|env\[|setattr\(|add_function|reg_func)", load_txt)) check(has_reg, g, "init.py 向 env 注册契约函数", "发现注册语句", "未发现任何注册语句") # load_path.py RBAC 路径显式枚举、无通配符 lp = os.path.join(HERE, "load_path.py") if os.path.exists(lp): txt = open(lp, "r", encoding="utf-8").read() wild = [] for i, line in enumerate(txt.splitlines(), 1): s = line.strip() if s.startswith("#"): continue for m in re.finditer(r"['\"]([^'\"]*)['\"]", line): v = m.group(1) if ("%" in v or "*" in v) and "/" in v: wild.append("%d:%s" % (i, v)) check(not wild, g, "load_path.py RBAC 路径无 %/* 通配符", "全部显式枚举", "发现通配路径: %s" % ";".join(wild[:10])) check(len(txt.strip()) > 0, g, "load_path.py 非空", "已注册路径", "文件为空") else: record("FAIL", g, "load_path.py 存在", "", "缺失 %s" % lp) # -------------------------------------------------------------------------- # F. 契约运行(import) # -------------------------------------------------------------------------- def group_f(): g = "F.契约运行" if REPO_ROOT not in sys.path: sys.path.insert(0, REPO_ROOT) # F1: errors.py 可独立 import 且 14 符号齐备 try: import importlib m = importlib.import_module("pbl_blueprint.errors") lack = [s for s in ERROR_SYMBOLS if not hasattr(m, s)] check(not lack, g, "pbl_blueprint.errors 导出 14 符号", "14 符号齐备", "缺符号: %s" % ",".join(lack)) # 语义抽检 r_ok = m.ok({"id": "1"}) check(m.is_ok(r_ok) and r_ok["code"] == m.ERR_OK, g, "ok() 返回 code=0", "%s" % (r_ok,), "异常: %s" % (r_ok,)) r_fail = m.fail(m.ERR_TENANT_MISSING) check(r_fail["code"] == m.ERR_TENANT_MISSING and not m.is_ok(r_fail), g, "fail(ERR_TENANT_MISSING) 非成功", "%s" % (r_fail,), "%s" % (r_fail,)) r_err = m.err() check(r_err["code"] == m.ERR_INTERNAL, g, "err() 缺省 ERR_INTERNAL", "%s" % (r_err,), "%s" % (r_err,)) try: raise m.PblBlueprintError(m.ERR_NOT_FOUND, "蓝图不存在") except m.PblBlueprintError as e: check(e.code == m.ERR_NOT_FOUND and e.to_dict()["code"] == m.ERR_NOT_FOUND, g, "PblBlueprintError 抛出与 to_dict()", "%s" % (e.to_dict(),), "%s" % (e.to_dict(),)) except Exception as e: # noqa: BLE001 record("FAIL", g, "pbl_blueprint.errors 可 import", "", "%s: %s" % (type(e).__name__, e)) # F2: 整包 import(依赖 sqlor/ahserver 运行时则 SKIP,不算 FAIL) try: import importlib importlib.import_module("pbl_blueprint") record("PASS", g, "import pbl_blueprint 整包", "成功") except ImportError as e: miss = str(e) if any(k in miss for k in ("sqlor", "ahserver", "sage", "sagelib", "DictObject", "DBPools")): record("SKIP", g, "import pbl_blueprint 整包", "运行时依赖缺失(CI 环境预期): %s" % miss) else: record("FAIL", g, "import pbl_blueprint 整包", "", "ImportError: %s" % miss) except Exception as e: # noqa: BLE001 record("FAIL", g, "import pbl_blueprint 整包", "", "%s: %s" % (type(e).__name__, e)) # -------------------------------------------------------------------------- # G. 租户 fail-closed # -------------------------------------------------------------------------- def group_g(): g = "G.租户fail-closed" tp = os.path.join(PKG_DIR, "tenant.py") if not os.path.exists(tp): record("FAIL", g, "tenant.py 存在", "", "缺失 %s" % tp) return txt = open(tp, "r", encoding="utf-8").read() check("ERR_TENANT_MISSING" in txt, g, "tenant.py 使用 ERR_TENANT_MISSING 拒绝", "fail-closed 拒绝路径存在", "未见 ERR_TENANT_MISSING") # 不得有默认租户兜底 fallback = re.search(r"tenant_id\s*=\s*tenant_id\s+or\s+['\"]", txt) or \ re.search(r"DEFAULT_TENANT\s*=\s*['\"][^'\"]+['\"]", txt) check(fallback is None, g, "tenant.py 无默认租户兜底", "缺失即拒绝,不静默填充", "发现默认租户兜底: %s" % (fallback.group(0) if fallback else "")) # 运行时抽检(可 import 时) if REPO_ROOT not in sys.path: sys.path.insert(0, REPO_ROOT) try: import importlib t = importlib.import_module("pbl_blueprint.tenant") fn = None for cand in ("require_tenant", "require_tenant_id", "get_tenant_id", "current_tenant_id"): if hasattr(t, cand): fn = getattr(t, cand) break if fn is None: record("SKIP", g, "租户缺失运行时拒绝", "未找到租户取值函数,静态检查已覆盖") else: try: fn({}) record("FAIL", g, "租户缺失运行时拒绝", "", "缺 tenant_id 未抛错/未拒绝") except Exception as e: # noqa: BLE001 record("PASS", g, "租户缺失运行时拒绝", "%s: %s" % (type(e).__name__, e)) except ImportError as e: record("SKIP", g, "租户缺失运行时拒绝", "运行时依赖缺失: %s" % e) except Exception as e: # noqa: BLE001 record("SKIP", g, "租户缺失运行时拒绝", "%s: %s" % (type(e).__name__, e)) # -------------------------------------------------------------------------- # H. DDL 生成 # -------------------------------------------------------------------------- def group_h(): g = "H.DDL生成" if not os.path.isdir(SQL_DIR): record("FAIL", g, "sql 目录", "缺失 %s" % SQL_DIR) return sqls = sorted([f for f in os.listdir(SQL_DIR) if f.endswith(".sql")]) check(len(sqls) >= 2, g, "sql/*.sql 存在(core + subobjects)", "%s" % ",".join(sqls), "实际 %d 个: %s" % (len(sqls), sqls)) blob = "" for fn in sqls: try: blob += open(os.path.join(SQL_DIR, fn), "r", encoding="utf-8").read() + "\n" except Exception as e: # noqa: BLE001 record("FAIL", g, "%s 可读" % fn, "", "%s" % e) ddl_tables = set(m.group(1).lower() for m in re.finditer( r"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`\"]?([A-Za-z0-9_]+)[`\"]?", blob, re.I)) missing = [t for t in EXPECTED_TABLES if t.lower() not in ddl_tables] check(not missing, g, "DDL 覆盖 11 张表", "CREATE TABLE %d 张" % len(ddl_tables), "DDL 缺表: %s" % ",".join(missing)) # 每张表 DDL 含 tenant_id no_tenant = [] for t in EXPECTED_TABLES: seg = re.search(r"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`\"]?%s[`\"]?\s*\((.*?)\n\)" % re.escape(t), blob, re.I | re.S) body = seg.group(1) if seg else "" if seg and "tenant_id" not in body: no_tenant.append(t) if no_tenant: record("FAIL", g, "DDL 各表含 tenant_id", "", "缺 tenant_id: %s" % ",".join(no_tenant)) else: record("PASS", g, "DDL 各表含 tenant_id", "已核验(或 DDL 由 models 生成)") # models 与 DDL 表集合一致 if os.path.isdir(MODELS_DIR): mset = set(f[:-5] for f in os.listdir(MODELS_DIR) if f.endswith(".json")) diff1 = sorted(mset - set(EXPECTED_TABLES)) check(not diff1, g, "models 无多余表", "一致", "models 多出: %s" % ",".join(diff1)) # -------------------------------------------------------------------------- # main # -------------------------------------------------------------------------- def main(): print("=" * 78) print("pbl_blueprint M1a 自查门禁") print("仓库根 : %s" % REPO_ROOT) print("包目录 : %s" % PKG_DIR) print("python : %s" % sys.version.replace("\n", " ")) print("=" * 78) if not os.path.isdir(REPO_ROOT): print("FATAL: 仓库根不存在 %s" % REPO_ROOT) return 2 for fn in (group_a, group_b, group_c, group_d, group_e, group_f, group_g, group_h): print("-" * 78) try: fn() except Exception as e: # noqa: BLE001 record("FAIL", fn.__name__, "检查组执行异常", "%s: %s" % (type(e).__name__, e)) total = len(RESULTS) n_pass = sum(1 for r in RESULTS if r[0] == "PASS") n_fail = sum(1 for r in RESULTS if r[0] == "FAIL") n_skip = sum(1 for r in RESULTS if r[0] == "SKIP") print("-" * 78) print("统计: 总计 %d | PASS %d | FAIL %d | SKIP %d" % (total, n_pass, n_fail, n_skip)) if n_fail: print("失败项清单:") for st, gp, nm, dt in RESULTS: if st == "FAIL": print(" - %s | %s | %s" % (gp, nm, (dt or "").replace("\n", " ")[:300])) print("结论: HAS FAILURES") # ★ 本轮修复项 B:有失败项必须 exit 非 0,否则门禁形同虚设 return 1 print("结论: ALL PASS%s" % ("(含 %d 项 SKIP,运行时依赖缺失,不计失败)" % n_skip if n_skip else "")) return 0 if __name__ == "__main__": try: rc = main() except KeyboardInterrupt: rc = 2 except Exception as _e: # noqa: BLE001 print("FATAL: 自查脚本自身异常: %s: %s" % (type(_e).__name__, _e)) rc = 2 print("exit_code=%s" % rc) sys.exit(rc)