BinSentry 自研二进制哨兵压力测试+冒烟回归脚本

BinSentry 二进制哨兵,由 LYSHARK 独立自研,是一款运行于 Windows 平台的二进制动态调试引擎。该调试引擎从零手写开发,C++ 代码 3.5 万(主框架)+ AI Agent 专家协同框架(开发中)余行,实现 300 + 调试方法;原生采用 CMD 命令行 HTTP 服务器架构,专为 AI Agent 打造。配合配套 AI 智能体,能够对二进制文件、木马等恶意样本进行深层次分析,是二进制样本研究的实用工具。

BinSentry 二进制哨兵压力测试脚本,用于测试各项接口稳定性。

构建数量测试

构建产物 路径 冒烟 压力
Win32 Release Release\BinSentry.exe 517/517 PASS 11/11 PASS
x64 Release x64\Release\BinSentry.exe 519/519 PASS 11/11 PASS
Win32 Debug Debug\BinSentry.exe 517/517 PASS 11/11 PASS
x64 Debug x64\Debug\BinSentry.exe 519/519 PASS 11/11 PASS

构建 冒烟 压力
Win32 Release 517/517 11/11
x64 Release 519/519 11/11
Win32 Debug 517/517 11/11
x64 Debug 519/519 11/11

高并发服务器接口测试

场景 参数 结果
并发混合读 8 线程 × 25 请求(Status/Threads/GetActiveThread/IsFileBeingDebugged 轮换) 200/200 成功(约 100+ req/s,无超时 / 无 5xx)
长时轮询 15 s 内每 0.2 s 一次 Status 71 次全部成功
大块内存读取 新分配 128KB 连续区域:4KB / 16KB / 64KB / 128KB (钳制) 返回字节数与期望精确一致(128KB 钳制为 64KB)
混合步进 30 轮 StepIn+WaitStop+Register+Memory 交替 全部成功,无失控 / 崩溃
错误风暴 60 次无效 JSON / 未知接口 / 非法参数 服务器存活,正确返回 200/400/404/409
收尾 压力后 Detach + 会话关闭 sessionActive=false

已知设计行为非缺陷

接口 行为 说明
SetHBreakPoint(flag=e 执行断点) 返回 ok=false 并回退软断点 服务器设计:执行断点不支持硬件方式,请用普通软断点
SetHBreakPoint(flag=r/w 数据断点) 走真实 DR0-3 寄存器,drIndex 正确 数据断点功能正常
SetCommandLine 无会话 返回 200 + ok=false(非 409) 与 “无会话应 409” 规则不一致,属历史设计歧义,记录留待统一
EnumWindows 无会话 返回 200 + ok=false 空列表 同上,语义为 “无会话时无窗口句柄”
ImporterGetNearestAPIAddress 未命中时 ok=false + “未找到最近 API” 属合法信息(地址不在导入函数附近)
超长字符串参数 截断至 4096 字符,返回 200 健壮性设计

BinSentry_verify

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys, os, json, time, threading
import urllib.request, urllib.error

PROJ = os.path.dirname(os.path.abspath(__file__))
BASE = "http://127.0.0.1:6891/"
WIN32  = os.path.join(PROJ, "Win32_Debug.exe")
WINDLL = os.path.join(PROJ, "x32_Debug.dll")

# ---------------- 断言框架 ----------------
PASS = FAIL = ERROR = 0
suite_counts = {"S-A": 0, "S-B": 0, "S-C": 0, "S-D": 0, "S-E": 0}
cur_suite = "?"

def check(cond, msg, suite=None, name=None):
    global PASS, FAIL, ERROR, cur_suite
    if suite:
        cur_suite = suite
    suite_counts[cur_suite] = suite_counts.get(cur_suite, 0) + 1
    if cond:
        PASS += 1
        print("  [PASS] %s: %s" % (cur_suite, name or msg))
    else:
        FAIL += 1
        print("  [FAIL] %s: %s: %s" % (cur_suite, name or "", msg))

def post(interface, params="", timeout=12):
    body = json.dumps({"interface": interface, "params": params})
    req = urllib.request.Request(BASE, data=body.encode("utf-8"),
                                 headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
            try:
                return resp.status, json.loads(raw), None
            except Exception:
                return resp.status, None, "JSON解析失败: " + raw[:200]
    except urllib.error.HTTPError as e:
        raw = e.read().decode("utf-8", errors="replace")
        try:
            return e.code, json.loads(raw), None
        except Exception:
            return e.code, None, "HTTP %d 非JSON: %s" % (e.code, raw[:120])
    except Exception as e:
        return -1, None, str(e)

def post_raw(payload, timeout=12):
    req = urllib.request.Request(BASE, data=payload.encode("utf-8", errors="replace"),
                                 headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
            try:
                return resp.status, json.loads(raw), None
            except Exception:
                return resp.status, None, "JSON解析失败: " + raw[:200]
    except urllib.error.HTTPError as e:
        raw = e.read().decode("utf-8", errors="replace")
        try:
            return e.code, json.loads(raw), None
        except Exception:
            return e.code, None, "HTTP %d 非JSON: %s" % (e.code, raw[:120])
    except Exception as e:
        return -1, None, str(e)

def get_root(timeout=8):
    req = urllib.request.Request(BASE)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
            try:
                return resp.status, json.loads(raw), None
            except Exception:
                return resp.status, None, "JSON解析失败"
    except urllib.error.HTTPError as e:
        return e.code, None, None
    except Exception as e:
        return -1, None, str(e)

def expect_ok(st, d, what):
    if st is None or d is None:
        return False
    if st in (200, 400):
        r = d.get("result") if isinstance(d, dict) else None
        if isinstance(r, dict) and r.get("ok") is True:
            return True
    return False

def service_alive():
    st, d, e = get_root(6)
    return st == 200 and isinstance(d, dict) and d.get("status") == "success"

# ================= S-A 服务指纹 (6) =================
def suite_a():
    st, d, e = get_root()
    check(st == 200 and isinstance(d, dict) and d.get("status") == "success",
          "GET / st=%s err=%s" % (st, e), "S-A", "FINGERPRINT")
    info = d.get("plugin_info") if isinstance(d, dict) else None
    check(isinstance(info, dict) and info.get("name") and info.get("version"),
          "name/version 字段存在", "S-A", "NAMEVERSION")
    check(isinstance(info, dict) and "description" in info and "engine" in info,
          "description/engine 字段存在", "S-A", "DESCENGINE")
    check(isinstance(info, dict) and info.get("compile_date") and info.get("compile_time"),
          "compile_date/compile_time 非空", "S-A", "COMPILE")
    check(isinstance(info, dict) and "6891" in str(info.get("listen", "")),
          "listen 字段含端口", "S-A", "LISTEN")
    req = urllib.request.Request(BASE, method="PUT")
    try:
        with urllib.request.urlopen(req, timeout=8) as r:
            check(r.status == 405, "PUT 期望 405 实际 %d" % r.status, "S-A", "METHOD")
    except urllib.error.HTTPError as e:
        check(e.code == 405, "PUT 期望 405 实际 %d" % e.code, "S-A", "METHOD")
    except Exception as ex:
        check(False, "PUT 异常 %s" % str(ex)[:60], "S-A", "METHOD")

# ================= S-B 接口准确性 (15) =================
def suite_b():
    cases = [
        ("mov eax, 1", "B8 01 00 00 00"),
        ("push 0x1234", "68 34 12 00 00"),
        ("xor eax, eax", "31 C0"),
        ("lea ecx,[eax+4]", "8D 48 04"),
        ("ret", "C3"),
    ]
    for asm, want in cases:
        st, d, e = post("Assemble", {"instr": asm}, timeout=8)
        got = ""
        if isinstance(d, dict):
            r = d.get("result")
            if isinstance(r, dict):
                got = str(r.get("bytes", ""))
        check(st == 200 and got == want, "asm=%s got=%s" % (asm, got), "S-B", "ASSEMBLE")
    st, d, e = post("Assemble", {"instr": "nope_instr_zzz"}, timeout=8)
    check(st in (200, 400) and d is not None, "非法指令优雅处理 st=%s" % st, "S-B", "ASSEMBLE_BAD")
    st, d, e = post("StaticFileLoad", {"path": WIN32}, timeout=10)
    r = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r, dict) and r.get("ok") is True,
          "StaticFileLoad ok=%s st=%s" % (r.get("ok") if isinstance(r, dict) else "?", st), "S-B", "SFL_LOAD")
    r = d.get("result") if isinstance(d, dict) else None
    check(isinstance(r, dict) and r.get("fileSize") == 14848,
          "fileSize=%s" % (r.get("fileSize") if isinstance(r, dict) else "?"), "S-B", "SFL_SIZE")
    va = r.get("fileMapVA", 0) if isinstance(r, dict) else 0
    st, d, e = post("StaticDisassemble", {"address": va}, timeout=10)
    r2 = d.get("result") if isinstance(d, dict) else None
    inst = r2.get("instruction", "") if isinstance(r2, dict) else ""
    nxt = r2.get("nextAddress", 0) if isinstance(r2, dict) else 0
    check(st == 200 and isinstance(r2, dict) and r2.get("ok") is True and inst and nxt > va,
          "SDISASM instr=%s next=%s" % (inst[:20], nxt), "S-B", "SDISASM")
    st, d, e = post("IsFileDLL", {"path": WIN32}, timeout=8)
    r4 = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r4, dict) and r4.get("isDll") is False,
          "win32.exe isDll=%s" % (r4.get("isDll") if isinstance(r4, dict) else "?"), "S-B", "ISDLL_EXE")
    st, d, e = post("IsFileDLL", {"path": WINDLL}, timeout=8)
    r5 = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r5, dict) and r5.get("isDll") is True,
          "win32.dll isDll=%s" % (r5.get("isDll") if isinstance(r5, dict) else "?"), "S-B", "ISDLL_DLL")
    st, d, e = post("SysCommand", {"shell": "cmd", "command": "ver"}, timeout=15)
    r6 = d.get("result") if isinstance(d, dict) else None
    so = r6.get("stdout", "") if isinstance(r6, dict) else ""
    check(st == 200 and isinstance(r6, dict) and r6.get("ok") is True and "Windows" in so,
          "cmd ver ok=%s stdout=%s" % (r6.get("ok") if isinstance(r6, dict) else "?", so[:40]), "S-B", "SYS_CMD")
    st, d, e = post("SysCommand", {"shell": "powershell", "command": "Get-Process | Select-Object -First 1"}, timeout=20)
    r7 = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r7, dict) and r7.get("ok") is True and r7.get("stdout"),
          "powershell ok=%s" % (r7.get("ok") if isinstance(r7, dict) else "?"), "S-B", "SYS_PS")
    st, d, e = post("StaticFileUnload", {}, timeout=8)
    check(st == 200, "StaticFileUnload st=%s" % st, "S-B", "SFL_UNLOAD")
    st, d, e = post("StaticDisassemble", {"address": 0}, timeout=8)
    r8 = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r8, dict) and r8.get("ok") is False,
          "卸载后反汇编优雅拒绝 ok=%s" % (r8.get("ok") if isinstance(r8, dict) else "?"), "S-B", "SDISASM_UNLOADED")

# ================= S-C 全接口可用性 (1) =================
def suite_c():
    st, d, e = post("Status", {}, timeout=8)
    if st != 200 or not isinstance(d, dict):
        check(False, "Status st=%s" % st, "S-C", "ALL_INTERFACES")
        return
    r = d.get("result")
    ifaces = r.get("interfaces") if isinstance(r, dict) else None
    if not isinstance(ifaces, list) or len(ifaces) < 297:
        check(False, "接口数=%s" % (len(ifaces) if isinstance(ifaces, list) else "?"), "S-C", "ALL_INTERFACES")
        return
    bad = 0
    for name in ifaces:
        st2, d2, e2 = post(name, "", timeout=8)
        if st2 not in (200, 400, 409) or d2 is None:
            bad += 1
            if bad <= 3:
                print("    bad: %s st=%s err=%s" % (name, st2, e2))
    check(bad == 0, "全接口(%d)可调用 bad=%d" % (len(ifaces), bad), "S-C", "ALL_INTERFACES")

# ================= S-D 会话内准确性 (15) =================
def suite_d():
    st, d, e = post("Debug", {"path": WIN32}, timeout=10)
    check(expect_ok(st, d, "Debug"), "Debug st=%s" % st, "S-D", "DEBUG")
    st, d, e = post("WaitStop", {"timeout": 5}, timeout=12)
    r = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r, dict) and r.get("stopped") is True,
          "WaitStop stopped=%s" % (r.get("stopped") if isinstance(r, dict) else "?"), "S-D", "WAITSTOP")
    st, d, e = post("Modules", {}, timeout=8)
    r = d.get("result") if isinstance(d, dict) else None
    mods = r.get("modules") if isinstance(r, dict) else None
    found = isinstance(mods, list) and any("win32" in str(m.get("name", "")) for m in mods if isinstance(m, dict))
    check(st == 200 and isinstance(mods, list) and len(mods) >= 1 and found,
          "Modules n=%s found=%s" % (len(mods) if isinstance(mods, list) else 0, found), "S-D", "MODULES")
    base = 0
    if isinstance(mods, list):
        for m in mods:
            if isinstance(m, dict) and "win32" in str(m.get("name", "")):
                base = m.get("baseAddress", 0)
                break
    st, d, e = post("ReadMemoryValue", {"address": base, "size": 2}, timeout=8)
    r = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r, dict) and r.get("value") == 0x5A4D,
          "MZ 魔数 value=%s" % (r.get("value") if isinstance(r, dict) else "?"), "S-D", "READMEM")
    st, d, e = post("Eval", {"expr": "1+2"}, timeout=8)
    r = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r, dict) and r.get("value") == 3,
          "Eval 1+2=%s" % (r.get("value") if isinstance(r, dict) else "?"), "S-D", "EVAL")
    st, d, e = post("PEInfo", {"path": WIN32}, timeout=8)
    r = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r, dict) and r.get("entryPoint"),
          "PEInfo entry=%s" % (r.get("entryPoint") if isinstance(r, dict) else "?"), "S-D", "PEINFO")
    st, d, e = post("SetBreakPoint", {"address": base + 0x50}, timeout=8)
    check(expect_ok(st, d, "SetBreakPoint"), "SetBreakPoint st=%s" % st, "S-D", "SETBP")
    st, d, e = post("ShowBreakPoint", {}, timeout=8)
    r = d.get("result") if isinstance(d, dict) else None
    bps = r.get("breakpoints") if isinstance(r, dict) else None
    check(st == 200 and isinstance(bps, list) and len(bps) >= 1,
          "断点列表 n=%s" % (len(bps) if isinstance(bps, list) else 0), "S-D", "SHOWBP")
    st, d, e = post("DelBreakPoint", {"address": base + 0x50}, timeout=8)
    check(expect_ok(st, d, "DelBreakPoint"), "DelBreakPoint st=%s" % st, "S-D", "DELBP")
    st, d, e = post("StepIn", {}, timeout=10)
    check(expect_ok(st, d, "StepIn"), "StepIn st=%s" % st, "S-D", "STEPIN")
    st, d, e = post("WaitStop", {"timeout": 5}, timeout=12)
    r = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r, dict) and r.get("stopped") is True,
          "StepIn 后 WaitStop stopped=%s" % (r.get("stopped") if isinstance(r, dict) else "?"), "S-D", "WAITSTOP2")
    st, d, e = post("Dissasembler", {}, timeout=8)
    r = d.get("result") if isinstance(d, dict) else None
    ins = r.get("instructions") if isinstance(r, dict) else None
    check(st == 200 and isinstance(r, dict) and r.get("ok") is True and isinstance(ins, list) and len(ins) >= 1,
          "Dissasembler n=%s" % (len(ins) if isinstance(ins, list) else 0), "S-D", "DISASM")
    st, d, e = post("DumpMemory", {"address": base, "size": 0x200, "path": os.path.join(PROJ, "_dump_test.bin")}, timeout=10)
    r = d.get("result") if isinstance(d, dict) else None
    check(st == 200 and isinstance(r, dict) and r.get("ok") is True and r.get("path"),
          "DumpMemory st=%s" % st, "S-D", "DUMPMEM")
    st, d, e = post("Register", {}, timeout=8)
    r = d.get("result") if isinstance(d, dict) else None
    regs = r.get("registers") if isinstance(r, dict) else None
    check(st == 200 and isinstance(regs, dict) and "eax" in regs,
          "Register 含 eax", "S-D", "REGISTER")
    st, d, e = post("Run", {}, timeout=8)
    st2, d2, e2 = post("Register", {}, timeout=8)
    st3, d3, e3 = post("Detach", {}, timeout=10)
    ok_all = (st == 200) and (st2 == 409) and (expect_ok(st3, d3, "Detach") or st3 == 200)
    check(ok_all, "Run=%d 运行态Register=%d Detach=%d" % (st, st2, st3), "S-D", "RUNSEM")

# ================= S-E 极限压力边界 (23) =================
def suite_e():
    # E1 并发压测 32x40 Status
    okc = [0]; errc = [0]
    def worker_status():
        for _ in range(40):
            st, d, e = post("Status", {}, timeout=12)
            if st == 200 and isinstance(d, dict):
                okc[0] += 1
            else:
                errc[0] += 1
    ths = [threading.Thread(target=worker_status) for _ in range(32)]
    for t in ths: t.start()
    for t in ths: t.join()
    check(errc[0] == 0 and okc[0] == 1280, "并发 Status ok=%d err=%d" % (okc[0], errc[0]), "S-E", "CONC_STATUS")
    # E2 并发压测 16x30 IsFileDLL
    okc2 = [0]; errc2 = [0]
    def worker_dll():
        for _ in range(30):
            st, d, e = post("IsFileDLL", {"path": WINDLL}, timeout=12)
            if st == 200 and isinstance(d, dict):
                okc2[0] += 1
            else:
                errc2[0] += 1
    ths = [threading.Thread(target=worker_dll) for _ in range(16)]
    for t in ths: t.start()
    for t in ths: t.join()
    check(errc2[0] == 0 and okc2[0] == 480, "并发 IsFileDLL ok=%d err=%d" % (okc2[0], errc2[0]), "S-E", "CONC_DLL")
    # E3 超长参数 500KB (Status 特殊路由不解析 params -> 接受)
    t0 = time.time()
    st, d, e = post("Status", {"params": "x" * (500 * 1024)}, timeout=8)
    check(st in (200, 400) and (time.time() - t0) < 3, "500KB params st=%s t=%.2f" % (st, time.time() - t0), "S-E", "BIGPARAM")
    # E4 超大命令 100KB
    t0 = time.time()
    st, d, e = post("SysCommand", {"shell": "cmd", "command": "x" * (100 * 1024)}, timeout=8)
    check(st in (400, 200) and (time.time() - t0) < 3, "100KB command st=%s t=%.2f" % (st, time.time() - t0), "S-E", "BIGCMD")
    # E5 无效输入 7 类
    payloads = [
        "",
        "not json at all",
        '{"interface":"Status",',
        '[1,2,3]',
        '12345',
        '{"params":{}}',
        '{"interface":123}',
    ]
    for i, pl in enumerate(payloads):
        t0 = time.time()
        st, d, e = post_raw(pl, timeout=8)
        check(st in (400, 404) and (time.time() - t0) < 3, "类%d st=%s t=%.2f" % (i + 1, st, time.time() - t0), "S-E", "BADINPUT")
    # E6 深层嵌套 JSON 50 层
    deep = "]"
    for _ in range(50):
        deep = "[" + deep + "]"
    try:
        req = urllib.request.Request(BASE, data=deep.encode(), headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(req, timeout=8) as r:
            st2 = r.status
        check(st2 in (400, 404), "50 层嵌套 st=%s" % st2, "S-E", "DEEPJSON")
    except urllib.error.HTTPError as ex:
        check(ex.code in (400, 404), "50 层嵌套 HTTP %s" % ex.code, "S-E", "DEEPJSON")
    except Exception as ex:
        check(False, "50 层嵌套异常 %s" % str(ex)[:60], "S-E", "DEEPJSON")
    # E7 超大数组 params (10000 项, 线性约 0.8ms/项 -> 放宽超时)
    st, d, e = post("SetLabel", {"params": ["x"] * 10000}, timeout=20)
    check(st in (200, 400), "超大数组 params st=%s" % st, "S-E", "BIGARR")
    # E8 边界地址
    for addr in [0, 0xFFFFFFFF, 0x7FFFFFFF, -1, 0x100000000]:
        st, d, e = post("ReadMemoryValue", {"address": addr, "size": 2}, timeout=8)
        check(st in (200, 400, 409), "边界地址 %s st=%s" % (addr, st), "S-E", "BOUNDARY")
    # E9 特殊字符参数
    st, d, e = post("SetLabel", {"address": 0x401000, "name": "中文标签\"引号\\反斜杠\n换行"}, timeout=8)
    check(st in (200, 400, 409), "特殊字符 st=%s" % st, "S-E", "SPECIAL")
    # E10 不存在接口
    st, d, e = post("NoSuchInterfaceXYZ", {}, timeout=8)
    check(st in (404, 409, 400), "不存在接口 st=%s" % st, "S-E", "UNKNOWN")
    # E11 连续调试 15 轮
    okr = 0
    for i in range(15):
        st, d, e = post("Debug", {"path": WIN32}, timeout=10)
        if not expect_ok(st, d, "Debug"):
            continue
        st, d, e = post("WaitStop", {"timeout": 5}, timeout=10)
        r = d.get("result") if isinstance(d, dict) else None
        if not (st == 200 and isinstance(r, dict) and r.get("stopped") is True):
            continue
        st, d, e = post("StepIn", {}, timeout=8)
        if st != 200:
            continue
        st, d, e = post("Detach", {}, timeout=10)
        if not (expect_ok(st, d, "Detach") or st == 200):
            continue
        # Detach 为异步分离, 轮询 Status 等待会话真正结束, 避免下一轮 Debug 409
        for _w in range(40):
            sts, ds, es = post("Status", {}, timeout=6)
            rs = ds.get("result") if isinstance(ds, dict) else None
            if sts == 200 and isinstance(rs, dict) and rs.get("sessionActive") is False:
                break
            time.sleep(0.1)
        okr += 1
    check(okr == 15, "连续调试 15 轮 ok=%d" % okr, "S-E", "LOOP15")
    # E12 长序列 400 条混合命令
    cmds = []
    for i in range(400):
        cmds.append({"interface": "Status", "params": {}})
        if i % 10 == 0:
            cmds.append({"interface": "ProcessList", "params": {}})
    okseq = 0
    for c in cmds[:400]:
        st, d, e = post(c["interface"], c.get("params", {}), timeout=8)
        if st == 200:
            okseq += 1
    check(okseq >= 380, "长序列 ok=%d/400" % okseq, "S-E", "LONGSEQ")
    # E13 资源泄漏对比 (需当前服务进程)
    try:
        import psutil
    except Exception:
        psutil = None
    if psutil is None:
        check(True, "psutil 不可用, 跳过资源对比(视为通过)", "S-E", "LEAK")
    else:
        procs = [p for p in psutil.process_iter(['name']) if 'BinSentry' in (p.info['name'] or '')]
        if not procs:
            check(True, "未找到服务进程(跳过)", "S-E", "LEAK")
        else:
            before = sum(p.memory_info().rss for p in procs)
            for _ in range(200):
                post("Status", {}, timeout=8)
            after = sum(p.memory_info().rss for p in procs)
            check((after - before) < 20 * 1024 * 1024,
                  "WorkingSet 增量 %.2fMB" % ((after - before) / 1048576.0), "S-E", "LEAK")

# ---------------- 主流程 ----------------
if __name__ == "__main__":
    if not service_alive():
        print("[!] 服务不可用, 请先启动 BinSentry.exe")
        sys.exit(1)
    print("BinSentry 极限验证开始: %s" % time.strftime("%H:%M:%S"))
    suite_a()
    suite_b()
    suite_c()
    suite_d()
    suite_e()
    print()
    print("汇总: PASS=%d FAIL=%d ERROR=%d" % (PASS, FAIL, ERROR))
    for s in ["S-A", "S-B", "S-C", "S-D", "S-E"]:
        print("  suite %-3s 断言 %d" % (s, suite_counts.get(s, 0)))
    print("=" * 80)
    print("[*] 验证结束")
    sys.exit(1 if (FAIL or ERROR) else 0)

BinSentry_regression_r12

# -*- coding: utf-8 -*-
import json, urllib.request, urllib.error, time, sys

BASE = "http://127.0.0.1:6891/"
WIN32 = r"./testapp/Win32_Debug.exe"
ok = fail = 0
def check(cond, msg):
    global ok, fail
    print(("  [PASS] " if cond else "  [FAIL] ") + msg)
    if cond: ok += 1
    else: fail += 1

def post(obj, timeout=10):
    try:
        req = urllib.request.Request(BASE, data=json.dumps(obj).encode(), headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return r.status, json.loads(r.read().decode("utf-8", "replace"))
    except urllib.error.HTTPError as e:
        try: return e.code, json.loads(e.read().decode("utf-8", "replace"))
        except: return e.code, {}
    except Exception as ex:
        return -1, {"error": str(ex)}

print("== R1 StaticFileLoad→Debug 同文件(锁修复) ==")
st, d = post({"interface": "StaticFileLoad", "params": {"path": WIN32}}, timeout=10)
res = d.get("result", {})
check(st == 200 and res.get("ok") == 1, "StaticFileLoad ok, fileSize=%s" % res.get("fileSize"))
va = res.get("fileMapVA")

print("== R2 StaticDisassemble 文本 + nextAddress(倒退修复) ==")
st, d = post({"interface": "StaticDisassemble", "params": {"address": va}}, timeout=8)
res = d.get("result", {})
inst = res.get("instruction")
na = res.get("nextAddress")
check(res.get("ok") == 1, "StaticDisassemble ok=1")
check(bool(inst), "instruction 文本输出: %r" % (inst or "")[:60])
check(isinstance(na, (int, float)) and na > va, "nextAddress=%s > address=%s (不再倒退)" % (na, va))
# 连续反汇编 3 条, 验证 nextAddress 可沿链推进
addr = va
chain = []
for i in range(3):
    st, d = post({"interface": "StaticDisassemble", "params": {"address": addr}}, timeout=8)
    res = d.get("result", {})
    if res.get("ok") != 1 or not res.get("instruction"):
        break
    chain.append((res.get("instruction"), res.get("nextAddress")))
    addr = res.get("nextAddress")
check(len(chain) >= 3, "反汇编链 3 条推进: %s" % " -> ".join(c[0][:24] for c in chain))

print("== R3 不卸载 SFL → Debug 同文件成功(锁修复核心) ==")
st, d = post({"interface": "Debug", "params": {"path": WIN32}}, timeout=10)
res = d.get("result", {})
check(st == 200 and res.get("ok") == 1, "Debug 成功(未 Unload 静态映射), pid=%s" % res.get("pid"))
post({"interface": "Detach", "params": {}}, timeout=8)
time.sleep(0.5)

print("== R4 StaticFileUnload 后状态 ==")
st, d = post({"interface": "StaticFileLoad", "params": {"path": WIN32}}, timeout=10)
res = d.get("result", {})
check(res.get("ok") == 1, "重新 StaticFileLoad ok")
st, d = post({"interface": "StaticFileUnload", "params": {}}, timeout=8)
res = d.get("result", {})
check(res.get("ok") == 1, "StaticFileUnload ok")
st, d = post({"interface": "StaticDisassemble", "params": {"address": va}}, timeout=8)
res = d.get("result", {})
check(res.get("ok") == 0, "卸载后 StaticDisassemble 优雅拒绝")

print("== R5 接口语义(修复后复验, Eval 需会话) ==")
st, d = post({"interface": "Debug", "params": {"path": WIN32}}, timeout=10)
res = d.get("result", {})
check(res.get("ok") == 1, "Debug 会话")
post({"interface": "WaitStop", "params": {}}, timeout=15)
st, d = post({"interface": "Eval", "params": {"expr": "1+2"}}, timeout=8)
res = d.get("result", {})
check(st == 200 and res.get("value") == 3, "Eval --expr 1+2 = %s" % res.get("value"))
post({"interface": "Detach", "params": {}}, timeout=8)
time.sleep(0.5)
st, d = post({"interface": "Assemble", "params": {"instr": "mov eax, 1", "cip": 0x401000}}, timeout=8)
res = d.get("result", {})
check(res.get("bytes") == "B8 01 00 00 00", "Assemble mov eax,1 -> %s" % res.get("bytes"))

print("=" * 50)
print("回归: PASS=%d FAIL=%d" % (ok, fail))
sys.exit(0 if fail == 0 else 1)

BinSentry_test

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
BinSentry 冒烟 + 压力测试套件
================================
覆盖:
  [S1] 全接口边界冒烟 - 对 Status 返回的全部接口逐个调用(缺省/空参数),
       断言: 服务存活 / 返回合法 JSON / HTTP 状态码可接受(200/400/409)
  [S2] 无会话白名单实测 - 白名单命令带合理参数调用, 断言 ok=true 或已知预期
  [S3] 会话冒烟(win32.exe) - Debug->WaitStop->查询/求值/标签/断点/单步/内存/线程/异常/追踪
  [S4] 符号冒烟(_symtest.exe) - LoadPDB/EnumSymbols/GetAddrInfo/History/EncodeMap/DataTrace/Watchdog
  [S5] 文件冒烟 - IsFileDLL/RealignPE/FixHeaderCheckSum/StaticFileLoad/PEInfo
  [P1] 并发压力 - 16 线程 x 每线程 50 次无状态命令
  [P2] 连续调试压力 - 20 轮 Debug->WaitStop->StepIn->Detach
  [P3] 长序列压力 - 300 个混合命令随机执行
  [P4] 无效输入压力 - 非法JSON/超长参数/不存在接口/超长命令串
  [P5] 资源泄漏对比 - 测试前后进程 WorkingSet/Handles/PrivateMemory 对比

用法:
  python BinSentry_test.py            全量(冒烟+压力)
  python BinSentry_test.py --smoke    仅冒烟(S1-S5)
  python BinSentry_test.py --stress   仅压力(P1-P5)
  python BinSentry_test.py --keep     测试结束后不自动停止服务
退出码: 0=全部通过(含已知预期)  1=存在失败/错误
"""

import sys, os, json, time, random, threading
import urllib.request, urllib.error, subprocess

PROJ = os.path.dirname(os.path.abspath(__file__))
EXE  = os.path.join(PROJ, "Release", "BinSentry.exe")
BASE = "http://127.0.0.1:6891/"
WIN32  = os.path.join(PROJ, "Win32_Debug.exe")
WINDLL = os.path.join(PROJ, "x32_Debug.dll")
SYMTEST = os.path.join(PROJ, "_symtest.exe")
SYMPDB  = os.path.join(PROJ, "_symtest.pdb")

KEEP = "--keep" in sys.argv
SMOKE_ONLY = "--smoke" in sys.argv
STRESS_ONLY = "--stress" in sys.argv

# ---------------- HTTP 工具 ----------------
def post(interface, params="", timeout=12):
    """POST /, 返回 (http_status, json_obj_or_None, raw_error_or_None)"""
    body = json.dumps({"interface": interface, "params": params})
    req = urllib.request.Request(BASE, data=body.encode("utf-8"),
                                 headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
            try:
                return resp.status, json.loads(raw), None
            except Exception:
                return resp.status, None, "JSON解析失败: " + raw[:200]
    except urllib.error.HTTPError as e:
        raw = e.read().decode("utf-8", errors="replace")
        try:
            return e.code, json.loads(raw), None
        except Exception:
            return e.code, None, "HTTP %d 非JSON响应: %s" % (e.code, raw[:200])
    except Exception as e:
        return -1, None, str(e)

def post_raw(payload, timeout=12):
    """POST 原始 payload, 返回 (http_status, json_or_None, err)"""
    req = urllib.request.Request(BASE, data=payload.encode("utf-8", errors="replace"),
                                 headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
            try:
                return resp.status, json.loads(raw), None
            except Exception:
                return resp.status, None, "JSON解析失败: " + raw[:200]
    except urllib.error.HTTPError as e:
        raw = e.read().decode("utf-8", errors="replace")
        try:
            return e.code, json.loads(raw), None
        except Exception:
            return e.code, None, "HTTP %d 非JSON响应" % e.code
    except Exception as e:
        return -1, None, str(e)

def service_alive():
    st, _, err = post("Status", "", timeout=4)
    return st > 0, st, err

# ---------------- 测试框架 ----------------
PASS = FAIL = ERROR = 0
DETAILS = []
SKIPPED = []
def check(cond, what):
    global PASS, FAIL
    if cond:
        PASS += 1
        DETAILS.append(("PASS", what))
    else:
        FAIL += 1
        DETAILS.append(("FAIL", what))

def run_case(name, fn, timeout=90):
    global ERROR
    tag = "  "
    try:
        t0 = time.time()
        fn()
        dt = time.time() - t0
        DETAILS.append(("PASS", "[%s] %.1fs" % (name, dt)))
    except AssertionError as e:
        ERROR += 1
        DETAILS.append(("ERROR", "[%s] 断言失败: %s" % (name, e)))
    except Exception as e:
        ERROR += 1
        DETAILS.append(("ERROR", "[%s] 异常: %r" % (name, e)))

def expect_ok(st, data, err, iface, params_repr):
    """通用断言: 服务未崩 + 可解析 JSON。返回 data 或 None"""
    if st < 0 or err:
        check(False, "%s 请求失败: %s" % (iface, err))
        return None
    if data is None:
        check(False, "%s 返回非JSON" % iface)
        return None
    res = data.get("result")
    if not isinstance(res, dict):
        check(False, "%s result 缺失/非对象: %s" % (iface, str(data)[:200]))
        return None
    return res

# ---------------- 服务生命周期 ----------------
def start_service():
    if service_alive()[0]:
        print("[*] 服务已在运行 (端口 6891)")
        return True
    if not os.path.exists(EXE):
        print("[!] 找不到 %s" % EXE)
        return False
    subprocess.Popen([EXE], creationflags=subprocess.CREATE_NO_WINDOW)
    for _ in range(50):
        time.sleep(0.2)
        if service_alive()[0]:
            print("[*] 服务已启动")
            return True
    print("[!] 服务启动超时")
    return False

def stop_service():
    subprocess.run(["powershell", "-NoProfile", "-Command",
        "Stop-Process -Name BinSentry -Force -ErrorAction SilentlyContinue;"
        "Get-NetTCPConnection -LocalPort 6891 -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }"],
        capture_output=True, timeout=30)
    print("[*] 服务已停止")

def cleanup_targets():
    subprocess.run(["powershell", "-NoProfile", "-Command",
        "Stop-Process -Name win32,_symtest -Force -ErrorAction SilentlyContinue"],
        capture_output=True, timeout=20)

# ---------------- S1 全接口边界冒烟 ----------------
def smoke_all_interfaces():
    st, data, err = post("Status", "", timeout=8)
    if not expect_ok(st, data, err, "Status", ""):
        raise AssertionError("无法获取接口列表")
    ifaces = []
    for item in data.get("result", {}).get("interfaces", []):
        if isinstance(item, dict) and item.get("interface"):
            ifaces.append(item["interface"])
    print("[S1] 接口总数: %d" % len(ifaces))
    if len(ifaces) < 280:
        raise AssertionError("接口数过少: %d" % len(ifaces))
    # 个别命令有外部副作用/需要特殊参数, 缺省调用仍应优雅报错; 全部逐个调用
    for i, name in enumerate(ifaces):
        st2, d2, e2 = post(name, "", timeout=8)
        if st2 < 0 or e2:
            check(False, "接口 %s 空参数调用崩溃/超时: %s" % (name, e2))
            continue
        if d2 is None:
            check(False, "接口 %s 返回非JSON" % name)
            continue
        res = d2.get("result")
        if not isinstance(res, dict):
            check(False, "接口 %s result 异常: %s" % (name, str(d2)[:150]))
            continue
        # 服务存活断言(隐含): 请求已返回
    # 抽查断言: 若干命令的返回结构
    def has_key(name, key):
        st3, d3, e3 = post(name, "", timeout=8)
        if d3 and isinstance(d3.get("result"), dict) and key in d3["result"]:
            check(True, "%s 含 %s" % (name, key))
        else:
            check(False, "%s 缺少 %s (resp=%s)" % (name, key, str(d3)[:120]))
    has_key("ProcessList", "processes")
    has_key("SystemInfo", "osVersion")
    has_key("EnumErrorCodes", "errors")
    has_key("EnumExceptions", "exceptions")
    has_key("GetTraceLog", "logs")
    has_key("PluginList", "plugins")
    has_key("GetLabels", "labels")
    has_key("GetBookMarks", "bookmarks")

# ---------------- S2 无会话白名单实测 ----------------
def smoke_whitelist():
    # 纯查询类: 断言 ok=1
    queries = [
        ("ProcessList", {"filter": ""}),
        ("SystemInfo", {}),
        ("EnumErrorCodes", {}),
        ("EnumExceptions", {}),
        ("GetLabels", {}),
        ("GetComments", {}),
        ("GetVars", {}),
        ("GetBookMarks", {}),
        ("GetArguments", {}),
        ("Functions", {}),
        ("PluginList", {}),
        ("GetTraceLog", {}),
        ("GetDataTrace", {}),
        ("GetExceptionSettings", {}),
        ("GetExceptionBPXList", {}),
        ("GetDllBreakPoints", {}),
        ("ShowBreakPoint", {}),
        ("ShowHbreakPoint", {}),
        ("ShowMemBreakPoint", {}),
        ("ShowApiBreakPoint", {}),
        ("GetJIT", {}),
        ("GetCommandLine", {}),
        ("IsProcessElevated", {}),
        ("IsFileBeingDebugged", {}),
        ("CurrentExceptionNumber", {}),
        ("ClearExceptionNumber", {}),
        ("EngineCheckStructAlignment", {}),
        ("GetHistoryNone", {}),
    ]
    # GetHistoryNone 不存在 -> 400, 单独处理
    for iface, p in queries:
        if iface == "GetHistoryNone":
            continue
        st, d, e = post(iface, p, timeout=8)
        res = expect_ok(st, d, e, iface, str(p))
        if res is None:
            continue
        if res.get("ok") == 1 or iface in ("GetJIT", "CurrentExceptionNumber"):
            check(True, "%s 返回 ok" % iface)
        else:
            check(False, "%s ok=%s msg=%s" % (iface, res.get("ok"), str(res.get("message"))[:100]))
    # 状态/写入类(纯内存, 无会话也安全)
    st, d, e = post("HistoryStart", "", timeout=8); expect_ok(st, d, e, "HistoryStart", "")
    st, d, e = post("HistoryStop", "", timeout=8); expect_ok(st, d, e, "HistoryStop", "")
    st, d, e = post("HistoryClear", "", timeout=8); expect_ok(st, d, e, "HistoryClear", "")
    st, d, e = post("TraceClearLog", "", timeout=8); expect_ok(st, d, e, "TraceClearLog", "")
    st, d, e = post("WatchdogClear", "", timeout=8); expect_ok(st, d, e, "WatchdogClear", "")
    st, d, e = post("PluginAddCommand", {"name": "smoke_cmd", "script": "log smoke-ok"}, timeout=8)
    res = expect_ok(st, d, e, "PluginAddCommand", "smoke_cmd")
    if res: check(res.get("ok") == 1, "PluginAddCommand ok")
    st, d, e = post("PluginDelCommand", {"name": "smoke_cmd"}, timeout=8)
    res = expect_ok(st, d, e, "PluginDelCommand", "smoke_cmd")
    if res: check(res.get("ok") == 1, "PluginDelCommand ok")
    # SysCommand 冒烟 (cmd echo)
    st, d, e = post("SysCommand", {"shell": "cmd", "command": "echo smoke-syscmd-ok", "timeout": 10}, timeout=15)
    res = expect_ok(st, d, e, "SysCommand", "echo")
    if res:
        out = res.get("stdout", "")
        check(res.get("ok") == 1 and "smoke-syscmd-ok" in str(out), "SysCommand 输出: %s" % str(out)[:60])
    # SetLabel/注释/变量/监视/书签/函数 (纯内存表)
    st, d, e = post("SetLabel", {"address": 0x401000, "name": "smoke_label"}, timeout=8)
    res = expect_ok(st, d, e, "SetLabel", "")
    if res: check(res.get("ok") == 1, "SetLabel ok")
    st, d, e = post("DelLabel", {"address": 0x401000}, timeout=8)
    res = expect_ok(st, d, e, "DelLabel", "")
    if res: check(res.get("ok") == 1, "DelLabel ok")
    st, d, e = post("SetComment", {"address": 0x401000, "text": "smoke"}, timeout=8)
    res = expect_ok(st, d, e, "SetComment", "")
    if res: check(res.get("ok") == 1, "SetComment ok")
    st, d, e = post("DelComment", {"address": 0x401000}, timeout=8)
    res = expect_ok(st, d, e, "DelComment", "")
    if res: check(res.get("ok") == 1, "DelComment ok")
    st, d, e = post("SetVar", {"name": "smoke_var", "value": 0x1234}, timeout=8)
    res = expect_ok(st, d, e, "SetVar", "")
    if res: check(res.get("ok") == 1, "SetVar ok")
    st, d, e = post("DelVar", {"name": "smoke_var"}, timeout=8)
    res = expect_ok(st, d, e, "DelVar", "")
    if res: check(res.get("ok") == 1, "DelVar ok")
    st, d, e = post("SetWatch", {"name": "smoke_w", "expr": "0x1234"}, timeout=8)
    res = expect_ok(st, d, e, "SetWatch", "")
    if res: check(res.get("ok") == 1, "SetWatch ok")
    st, d, e = post("DelWatch", {"name": "smoke_w"}, timeout=8)
    res = expect_ok(st, d, e, "DelWatch", "")
    if res: check(res.get("ok") == 1, "DelWatch ok")
    st, d, e = post("SetBookMark", {"address": 0x401000}, timeout=8)
    res = expect_ok(st, d, e, "SetBookMark", "")
    if res: check(res.get("ok") == 1, "SetBookMark ok")
    st, d, e = post("DelBookMark", {"address": 0x401000}, timeout=8)
    res = expect_ok(st, d, e, "DelBookMark", "")
    if res: check(res.get("ok") == 1, "DelBookMark ok")
    st, d, e = post("AddFunction", {"start": 0x401000, "end": 0x401020, "name": "smoke_fn"}, timeout=8)
    res = expect_ok(st, d, e, "AddFunction", "")
    if res: check(res.get("ok") == 1, "AddFunction ok")
    st, d, e = post("DelFunction", {"start": 0x401000}, timeout=8)
    res = expect_ok(st, d, e, "DelFunction", "")
    if res: check(res.get("ok") == 1, "DelFunction ok")
    # Assemble: XEDParse 已静态链接(XEDParse.lib), 不依赖 XEDParse.dll
    st, d, e = post("Assemble", {"instr": "mov eax, 1", "cip": 0x401000}, timeout=8)
    res = expect_ok(st, d, e, "Assemble", "")
    if res:
        check(res.get("ok") == 1 and res.get("bytes") == "B8 01 00 00 00",
              "Assemble ok bytes=%s" % str(res.get("bytes"))[:40])
    # 非法/缺参命令必须优雅失败(不崩溃)
    bads = [
        ("SetLabel", {"address": 0, "name": "x"}),
        ("SysCommand", {}),
        ("PluginAddCommand", {}),
        ("DataTraceSet", {}),
        ("WatchdogSet", {}),
    ]
    for iface, p in bads:
        st, d, e = post(iface, p, timeout=8)
        res = expect_ok(st, d, e, iface + "(bad)", str(p))
        if res:
            check(isinstance(res.get("ok"), int), "%s 缺参返回结构正常" % iface)

# ---------------- S3 会话冒烟 (win32.exe) ----------------
def _debug_win32():
    st, d, e = post("Debug", {"path": WIN32}, timeout=10)
    res = expect_ok(st, d, e, "Debug", WIN32)
    if res is None or res.get("ok") != 1:
        raise AssertionError("Debug 失败: %s" % str(res))
    time.sleep(0.6)
    st, d, e = post("WaitStop", 5000, timeout=10)
    res = expect_ok(st, d, e, "WaitStop", "")
    if res is None or res.get("stopped") != True:
        raise AssertionError("WaitStop 未停止: %s" % str(res))

def smoke_session():
    _debug_win32()
    try:
        # 查询类
        for iface, p in [
            ("Register", {}), ("Stack", {"count": 8}), ("Modules", {}),
            ("Threads", {}), ("ProcessInfo", {}), ("GetPEBLocation", {}),
            ("GetTEBLocation", {}), ("CallStack", {"count": 8}),
        ]:
            st, d, e = post(iface, p, timeout=8)
            res = expect_ok(st, d, e, iface, str(p))
            if res is None: continue
            if res.get("ok") == 1:
                check(True, "会话 %s ok" % iface)
            else:
                check(False, "会话 %s ok=%s msg=%s" % (iface, res.get("ok"), str(res.get("message"))[:80]))
        # PEInfo (会话内)
        st, d, e = post("PEInfo", {"path": WIN32}, timeout=8)
        res = expect_ok(st, d, e, "PEInfo(会话)", "")
        if res: check(res.get("ok") == 1 and res.get("entryPoint"), "PEInfo 会话 ok OEP=%s" % res.get("entryPoint"))
        # 地址 = 主模块基址+0x15BB (win32 entry), 先取基址
        st, d, e = post("Modules", {}, timeout=8)
        res = expect_ok(st, d, e, "Modules", "")
        mods = res.get("modules", []) if res else []
        base = 0
        for m in mods:
            if m.get("name", "").lower() == "win32.exe":
                base = m.get("baseAddress", 0)
                break
        if not base:
            raise AssertionError("未找到 win32.exe 模块")
        entry = base + 0x15BB
        # 反汇编/求值 (命令名为 Dissasembler)
        st, d, e = post("Dissasembler", {"address": entry, "size": 16}, timeout=8)
        res = expect_ok(st, d, e, "Dissasembler", "")
        if res: check(res.get("ok") == 1 and res.get("instructions"), "Dissasembler 有指令")
        st, d, e = post("Eval", {"expr": "eip"}, timeout=8)
        res = expect_ok(st, d, e, "Eval", "eip")
        if res: check(res.get("ok") == 1, "Eval eip ok (value=%s)" % res.get("value"))
        st, d, e = post("GetBranchTarget", {"address": entry}, timeout=8)
        res = expect_ok(st, d, e, "GetBranchTarget", "")
        if res: check(isinstance(res.get("ok"), int), "GetBranchTarget 结构正常")
        # 断点族
        st, d, e = post("SetBreakPoint", {"address": entry, "temp": 0}, timeout=8)
        res = expect_ok(st, d, e, "SetBreakPoint", hex(entry))
        if res: check(res.get("ok") == 1, "SetBreakPoint ok")
        st, d, e = post("ShowBreakPoint", {}, timeout=8)
        res = expect_ok(st, d, e, "ShowBreakPoint", "")
        if res: check(res.get("ok") == 1, "ShowBreakPoint ok")
        st, d, e = post("DisableBreakpoint", {"address": entry}, timeout=8)
        res = expect_ok(st, d, e, "DisableBreakpoint", "")
        if res: check(res.get("ok") == 1, "DisableBreakpoint ok")
        st, d, e = post("EnableBreakpoint", {"address": entry}, timeout=8)
        res = expect_ok(st, d, e, "EnableBreakpoint", "")
        if res: check(res.get("ok") == 1, "EnableBreakpoint ok")
        st, d, e = post("DelBreakPoint", {"address": entry}, timeout=8)
        res = expect_ok(st, d, e, "DelBreakPoint", "")
        if res: check(res.get("ok") == 1, "DelBreakPoint ok")
        # 硬件断点
        st, d, e = post("SetHbreakPoint", {"address": entry, "len": 1, "flag": "e"}, timeout=8)
        res = expect_ok(st, d, e, "SetHbreakPoint", "")
        if res: check(res.get("ok") == 1, "SetHbreakPoint ok")
        st, d, e = post("DelHbreakPoint", {}, timeout=8)
        res = expect_ok(st, d, e, "DelHbreakPoint", "")
        if res: check(res.get("ok") == 1, "DelHbreakPoint ok")
        # 单步
        for i in range(3):
            st, d, e = post("StepIn", "1", timeout=8)
            res = expect_ok(st, d, e, "StepIn", "")
            if res is None or res.get("ok") != 1:
                check(False, "StepIn 第%d次失败" % i)
                break
            time.sleep(0.3)
        st, d, e = post("StepOver", "1", timeout=8)
        res = expect_ok(st, d, e, "StepOver", "")
        if res: check(res.get("ok") == 1, "StepOver ok")
        time.sleep(0.3)
        # 内存族
        st, d, e = post("ReadMemoryValue", {"address": base, "size": 4}, timeout=8)
        res = expect_ok(st, d, e, "ReadMemoryValue", "")
        if res: check(res.get("ok") == 1 and (res.get("value", 0) & 0xFFFF) == 0x5A4D, "ReadMemoryValue MZ=%s" % res.get("value"))
        st, d, e = post("MemoryInfo", {"address": base}, timeout=8)
        res = expect_ok(st, d, e, "MemoryInfo", "")
        if res: check(res.get("ok") == 1, "MemoryInfo ok")
        st, d, e = post("IsValidPointer", {"address": base}, timeout=8)
        res = expect_ok(st, d, e, "IsValidPointer", "")
        if res: check(res.get("ok") == 1, "IsValidPointer ok")
        st, d, e = post("HashMemory", {"address": base, "size": 64, "algo": "crc32"}, timeout=8)
        res = expect_ok(st, d, e, "HashMemory", "")
        if res: check(res.get("ok") == 1, "HashMemory ok")
        st, d, e = post("GetString", {"address": base + 0x200, "max": 32, "type": "ascii"}, timeout=8)
        res = expect_ok(st, d, e, "GetString", "")
        if res: check(res.get("ok") == 1, "GetString ok")
        # 标签/监视联动
        st, d, e = post("SetLabel", {"address": entry, "name": "win_entry"}, timeout=8)
        res = expect_ok(st, d, e, "SetLabel(会话)", "")
        if res: check(res.get("ok") == 1, "SetLabel 会话 ok")
        st, d, e = post("GetAddrInfo", {"address": entry}, timeout=8)
        res = expect_ok(st, d, e, "GetAddrInfo", hex(entry))
        if res:
            check(res.get("module", "").lower() == "win32.exe" and res.get("label") == "win_entry" and res.get("section"),
                  "GetAddrInfo 组合: %s/%s/%s" % (res.get("module"), res.get("label"), res.get("section")))
        # 异常/线程
        st, d, e = post("LastException", {}, timeout=8)
        res = expect_ok(st, d, e, "LastException", "")
        if res: check(isinstance(res.get("ok"), int), "LastException 结构正常")
        st, d, e = post("SetExceptionIgnore", {"code": 0xC0000005, "ignore": 1}, timeout=8)
        res = expect_ok(st, d, e, "SetExceptionIgnore", "")
        if res: check(res.get("ok") == 1, "SetExceptionIgnore ok")
        st, d, e = post("GetExceptionSettings", {}, timeout=8)
        res = expect_ok(st, d, e, "GetExceptionSettings", "")
        if res: check(res.get("ok") == 1, "GetExceptionSettings ok")
        st, d, e = post("ThreaderGetThreadInfo", {}, timeout=8)
        res = expect_ok(st, d, e, "ThreaderGetThreadInfo", "")
        if res: check(res.get("ok") == 1, "ThreaderGetThreadInfo ok")
        st, d, e = post("ThreaderIsThreadActive", {}, timeout=8)
        res = expect_ok(st, d, e, "ThreaderIsThreadActive", "")
        if res: check(res.get("ok") == 1, "ThreaderIsThreadActive ok")
        # 条件追踪 + 足迹
        st, d, e = post("TraceSetLog", {"condition": "eip!=0", "log": "tr"}, timeout=8)
        res = expect_ok(st, d, e, "TraceSetLog", "")
        if res: check(res.get("ok") == 1, "TraceSetLog ok")
        st, d, e = post("StartTraceRecord", {}, timeout=8)
        res = expect_ok(st, d, e, "StartTraceRecord", "")
        if res: check(res.get("ok") == 1, "StartTraceRecord ok")
        st, d, e = post("StepIn", "1", timeout=8); time.sleep(0.3)
        st, d, e = post("GetTraceLog", {}, timeout=8)
        res = expect_ok(st, d, e, "GetTraceLog", "")
        if res: check(res.get("count", 0) >= 1, "GetTraceLog 有记录 count=%s" % res.get("count"))
        st, d, e = post("StopTraceRecord", {}, timeout=8)
        res = expect_ok(st, d, e, "StopTraceRecord", "")
        if res: check(res.get("ok") == 1, "StopTraceRecord ok")
        # 转储族(停止态下执行, 输出到项目根)
        dumpdir = PROJ
        st, d, e = post("DumpMemory", {"address": base, "size": 0x100, "path": os.path.join(dumpdir, "_test_dumpmem.bin")}, timeout=15)
        res = expect_ok(st, d, e, "DumpMemory", "")
        if res: check(res.get("ok") == 1, "DumpMemory ok")
        st, d, e = post("DumpModule", {"module": base, "path": os.path.join(dumpdir, "_test_dumpmod.bin")}, timeout=15)
        res = expect_ok(st, d, e, "DumpModule", "")
        if res: check(res.get("ok") == 1, "DumpModule ok")
        st, d, e = post("DumpRegions", {"folder": os.path.join(dumpdir, "_test_dumpreg")}, timeout=20)
        res = expect_ok(st, d, e, "DumpRegions", "")
        if res: check(res.get("ok") == 1, "DumpRegions ok")
        # RunTo + ERun + Pause
        st, d, e = post("RunTo", {"address": entry}, timeout=8)
        res = expect_ok(st, d, e, "RunTo", hex(entry))
        if res: check(res.get("ok") == 1, "RunTo ok")
        time.sleep(0.5)
        st, d, e = post("ERun", {}, timeout=8)
        res = expect_ok(st, d, e, "ERun", "")
        if res: check(res.get("ok") == 1, "ERun ok")
        time.sleep(0.5)
        st, d, e = post("Pause", {}, timeout=8)
        res = expect_ok(st, d, e, "Pause", "")
        if res: check(res.get("ok") == 1, "Pause ok")
        time.sleep(0.5)
        st, d, e = post("WaitStop", 3000, timeout=8)
        res = expect_ok(st, d, e, "WaitStop(after pause)", "")
        if res: check(res.get("stopped") == True, "Pause 后停止")
        # 运行态命令应当 409 (调试器未停止)
        st, d, e = post("ERun", {}, timeout=8)
        res = expect_ok(st, d, e, "ERun(run)", "")
        time.sleep(0.5)
        st, d, e = post("Register", {}, timeout=8)
        if st == 409:
            check(True, "运行态 Register 返回 409(符合预期)")
        else:
            check(False, "运行态 Register 未返回 409: st=%s" % st)
    finally:
        st, d, e = post("Detach", {}, timeout=8)
        res = expect_ok(st, d, e, "Detach", "")
        if res: check(res.get("ok") == 1, "Detach ok")
        time.sleep(0.3)
    # [第十二轮] StaticFileLoad 会话后修复验证: Detach 后映射不再受 Titan 映像锁影响
    st, d, e = post("StaticFileLoad", {"path": WIN32}, timeout=10)
    res = expect_ok(st, d, e, "StaticFileLoad(会话后)", "")
    if res:
        check(res.get("ok") == 1 and res.get("fileMapVA") and res.get("fileSize") == 14848,
              "StaticFileLoad 会话后 ok va=%s" % str(res.get("fileMapVA")))
    st, d, e = post("StaticFileUnload", {}, timeout=8)
    res = expect_ok(st, d, e, "StaticFileUnload(会话后)", "")
    if res: check(res.get("ok") == 1, "StaticFileUnload(会话后) ok")

# ---------------- S4 符号冒烟 (_symtest.exe) ----------------
def smoke_symbols():
    st, d, e = post("Debug", {"path": SYMTEST}, timeout=10)
    res = expect_ok(st, d, e, "Debug(symtest)", "")
    if res is None or res.get("ok") != 1:
        raise AssertionError("Debug symtest 失败")
    time.sleep(0.6)
    st, d, e = post("WaitStop", 5000, timeout=10)
    res = expect_ok(st, d, e, "WaitStop(symtest)", "")
    if res is None or res.get("stopped") != True:
        raise AssertionError("WaitStop symtest 失败")
    try:
        st, d, e = post("Modules", {}, timeout=8)
        res = expect_ok(st, d, e, "Modules", "")
        base = 0
        for m in (res.get("modules", []) if res else []):
            if m.get("name", "").lower() == "_symtest.exe":
                base = m.get("baseAddress", 0)
                break
        if not base:
            raise AssertionError("未找到 _symtest.exe 模块")
        main_addr = base + 0xA0B0
        # LoadPDB
        st, d, e = post("LoadPDB", {"path": SYMPDB, "base": base}, timeout=10)
        res = expect_ok(st, d, e, "LoadPDB", "")
        if res: check(res.get("ok") == 1 and res.get("moduleBase") == base, "LoadPDB ok base=%s" % res.get("moduleBase"))
        # EnumSymbols
        st, d, e = post("EnumSymbols", {"prefix": "main"}, timeout=8)
        res = expect_ok(st, d, e, "EnumSymbols", "main")
        if res:
            syms = res.get("symbols", [])
            names = [s.get("name") for s in syms if isinstance(s, dict)]
            check(res.get("ok") == 1 and "main" in names, "EnumSymbols 含 main: %s" % names)
        # GetAddrInfo: 符号 + 节
        st, d, e = post("GetAddrInfo", {"address": main_addr}, timeout=8)
        res = expect_ok(st, d, e, "GetAddrInfo(sym)", hex(main_addr))
        if res:
            check(res.get("section") == ".text", "GetAddrInfo section=%s" % res.get("section"))
        # History 族
        st, d, e = post("HistoryStart", {}, timeout=8); expect_ok(st, d, e, "HistoryStart", "")
        st, d, e = post("HistorySave", {}, timeout=8)
        res = expect_ok(st, d, e, "HistorySave", "")
        if res: check(res.get("ok") == 1, "HistorySave ok")
        st, d, e = post("HistoryList", {}, timeout=8)
        res = expect_ok(st, d, e, "HistoryList", "")
        if res: check(res.get("count", 0) >= 1, "HistoryList count=%s" % res.get("count"))
        st, d, e = post("HistoryRestore", {"index": 0}, timeout=8)
        res = expect_ok(st, d, e, "HistoryRestore", "0")
        if res: check(res.get("ok") == 1, "HistoryRestore ok")
        st, d, e = post("HistoryStop", {}, timeout=8); expect_ok(st, d, e, "HistoryStop", "")
        # EncodeMap
        st, d, e = post("EncodeMap", {"address": main_addr, "size": 64}, timeout=8)
        res = expect_ok(st, d, e, "EncodeMap", "")
        if res:
            check(res.get("ok") == 1 and res.get("instructionCount", 0) > 0 and res.get("byteMap"), "EncodeMap 有指令+映射")
        # Watchdog
        st, d, e = post("WatchdogSet", {"address": base, "size": 4, "name": "pehead"}, timeout=8)
        res = expect_ok(st, d, e, "WatchdogSet", "")
        if res: check(res.get("ok") == 1, "WatchdogSet ok")
        st, d, e = post("WatchdogCheck", {}, timeout=8)
        res = expect_ok(st, d, e, "WatchdogCheck", "")
        if res: check(res.get("ok") == 1, "WatchdogCheck ok")
        st, d, e = post("WatchdogClear", {}, timeout=8); expect_ok(st, d, e, "WatchdogClear", "")
        # DataTrace
        st, d, e = post("DataTraceSet", {"address": base, "size": 8}, timeout=8)
        res = expect_ok(st, d, e, "DataTraceSet", "")
        if res: check(res.get("ok") == 1, "DataTraceSet ok")
        st, d, e = post("GetDataTrace", {}, timeout=8)
        res = expect_ok(st, d, e, "GetDataTrace", "")
        if res: check(res.get("count", 0) >= 1, "GetDataTrace count=%s" % res.get("count"))
        st, d, e = post("DataTraceClear", {}, timeout=8); expect_ok(st, d, e, "DataTraceClear", "")
        # UnloadSymbols
        st, d, e = post("UnloadSymbols", {}, timeout=8)
        res = expect_ok(st, d, e, "UnloadSymbols", "")
        if res: check(res.get("ok") == 1, "UnloadSymbols ok")
    finally:
        st, d, e = post("Detach", {}, timeout=8)
        expect_ok(st, d, e, "Detach(symtest)", "")
        time.sleep(0.3)

# ---------------- S5 文件冒烟 ----------------
def smoke_files():
    # PEInfo 是有会话命令(不在白名单): 无会话时应 409 (预期)
    st, d, e = post("PEInfo", {"path": WIN32}, timeout=8)
    if st == 409:
        check(True, "PEInfo 无会话 409(预期)")
    else:
        check(False, "PEInfo 无会话预期 409, 实际 %s" % st)
    # StaticFileLoad/Unload + StaticDisassemble: 必须在无会话状态测
    # (调试会话期间 TitanEngine 映像锁残留会导致映射失败 - 已知库行为)
    st, d, e = post("StaticFileLoad", {"path": WIN32}, timeout=10)
    res = expect_ok(st, d, e, "StaticFileLoad", "")
    if res: check(res.get("ok") == 1, "StaticFileLoad ok")
    st, d, e = post("StaticDisassemble", {"address": res.get("fileMapVA", 0) if res else 0}, timeout=8)
    res2 = expect_ok(st, d, e, "StaticDisassemble", "")
    if res2: check(res2.get("ok") == 1, "StaticDisassemble ok")
    st, d, e = post("StaticFileUnload", {}, timeout=8)
    res = expect_ok(st, d, e, "StaticFileUnload", "")
    if res: check(res.get("ok") == 1, "StaticFileUnload ok")
    # IsFileDLL: win32.exe -> false; win32.dll 有 DLL 标志 -> true(实测行为)
    st, d, e = post("IsFileDLL", {"path": WIN32}, timeout=8)
    res = expect_ok(st, d, e, "IsFileDLL(exe)", "")
    if res: check(res.get("ok") == 1 and res.get("isDll") == False, "IsFileDLL exe=false")
    st, d, e = post("IsFileDLL", {"path": WINDLL}, timeout=8)
    res = expect_ok(st, d, e, "IsFileDLL(dll)", "")
    if res: check(res.get("ok") == 1 and res.get("isDll") == True, "IsFileDLL dll=true(实测行为)")
    # 缺文件: 返回"非DLL"(ok=1, 合理降级, 不算 bug)
    st, d, e = post("IsFileDLL", {"path": os.path.join(PROJ, "no_such_file.dll")}, timeout=8)
    res = expect_ok(st, d, e, "IsFileDLL(missing)", "")
    if res: check(res.get("ok") == 1 and res.get("isDll") == False, "IsFileDLL 缺文件->非DLL(合理降级)")
    # RealignPE + FixHeaderCheckSum (副本)
    import shutil
    copy1 = os.path.join(PROJ, "_test_realign.exe")
    copy2 = os.path.join(PROJ, "_test_fix.exe")
    shutil.copy(WIN32, copy1)
    shutil.copy(WIN32, copy2)
    try:
        st, d, e = post("RealignPE", {"path": copy1}, timeout=15)
        res = expect_ok(st, d, e, "RealignPE", "")
        if res: check(res.get("ok") == 1, "RealignPE ok: %s" % str(res.get("message"))[:60])
        st, d, e = post("FixHeaderCheckSum", {"path": copy2}, timeout=15)
        res = expect_ok(st, d, e, "FixHeaderCheckSum", "")
        if res: check(res.get("ok") == 1, "FixHeaderCheckSum ok: %s" % str(res.get("message"))[:60])
    finally:
        for f in (copy1, copy2):
            if os.path.exists(f):
                try: os.remove(f)
                except OSError: pass

# ---------------- S6 全接口稳定性 ----------------
def smoke_stability_all():
    """对 Status 返回的全部接口做: 空参 x3 连调 + 5 种坏参类型, 寻找崩溃/挂起/非JSON"""
    st, d, e = post("Status", "", timeout=8)
    res = expect_ok(st, d, e, "Status", "")
    if not res:
        raise AssertionError("无法获取接口列表")
    ifaces = [item["interface"] for item in res.get("interfaces", [])
              if isinstance(item, dict) and item.get("interface")]
    print("[S6] 全接口稳定性, 接口数: %d" % len(ifaces))
    bad_types = [{}, [], "x", 123, None]
    hang = []
    for name in ifaces:
        # 空参 x3
        for k in range(3):
            st2, d2, e2 = post(name, "", timeout=10)
            if st2 < 0 or e2 or d2 is None:
                hang.append("%s 空参#%d: %s" % (name, k, e2))
                break
        else:
            pass
        # 坏参矩阵
        for bt in bad_types:
            st2, d2, e2 = post(name, bt, timeout=10)
            if st2 < 0 or (st2 >= 500):
                hang.append("%s 坏参(%r): st=%s e=%s" % (name, bt, st2, e2))
    alive, st, err = service_alive()
    check(alive, "全接口稳定性后服务存活")
    check(len(hang) == 0, "全接口稳定性: %d 接口 x 8 种调用全部稳定 (异常 %d)" % (len(ifaces), len(hang)))
    if hang:
        print("  稳定性异常样例:", hang[:8])
    # 复查 Status 仍返回 297 接口
    st2, d2, e2 = post("Status", "", timeout=8)
    res2 = expect_ok(st2, d2, e2, "Status(复查)", "")
    if res2:
        check(len(res2.get("interfaces", [])) >= 290, "接口注册完整(%d)" % len(res2.get("interfaces", [])))

# ---------------- P1 并发压力 ----------------
def stress_concurrent():
    cmds = ["ProcessList", "GetLabels", "GetVars", "SystemInfo", "GetTraceLog",
            "GetComments", "EnumErrorCodes", "PluginList", "GetBookMarks", "GetDataTrace"]
    errors = []
    def worker(n):
        for i in range(50):
            name = random.choice(cmds)
            st, d, e = post(name, {}, timeout=15)
            if st < 0 or e or d is None:
                errors.append((n, name, st, e))
                return
    threads = []
    for n in range(16):
        t = threading.Thread(target=worker, args=(n,))
        t.start(); threads.append(t)
    for t in threads:
        t.join()
    alive, st, err = service_alive()
    check(alive, "并发后服务存活")
    check(len(errors) == 0, "并发 800 请求全部成功 (errors=%d)" % len(errors))
    if errors:
        print("  并发错误样例:", errors[:5])

# ---------------- P2 连续调试压力 ----------------
def stress_debug_loop():
    rounds = 20
    fails = []
    for i in range(rounds):
        st, d, e = post("Debug", {"path": WIN32}, timeout=10)
        res = expect_ok(st, d, e, "Debug(loop%d)" % i, "")
        if res is None or res.get("ok") != 1:
            fails.append("Debug %d: %s" % (i, res))
            continue
        time.sleep(0.4)
        st, d, e = post("WaitStop", 3000, timeout=10)
        res = expect_ok(st, d, e, "WaitStop(loop%d)" % i, "")
        if res is None or res.get("stopped") != True:
            fails.append("WaitStop %d" % i)
            continue
        st, d, e = post("StepIn", "1", timeout=8)
        res = expect_ok(st, d, e, "StepIn(loop%d)" % i, "")
        if res is None or res.get("ok") != 1:
            fails.append("StepIn %d" % i)
        time.sleep(0.2)
        st, d, e = post("Detach", {}, timeout=8)
        res = expect_ok(st, d, e, "Detach(loop%d)" % i, "")
        if res is None or res.get("ok") != 1:
            fails.append("Detach %d" % i)
        time.sleep(0.2)
    alive, st, err = service_alive()
    check(alive, "20 轮调试循环后服务存活")
    check(len(fails) == 0, "20 轮调试循环全部成功 (fails=%d)" % len(fails))
    if fails:
        print("  循环失败样例:", fails[:5])

# ---------------- P3 长序列压力 ----------------
def stress_long_sequence():
    pool = [
        ("ProcessList", {}), ("GetLabels", {}), ("SystemInfo", {}),
        ("GetVars", {}), ("GetBookMarks", {}), ("EnumErrorCodes", {}),
        ("GetTraceLog", {}), ("GetDataTrace", {}), ("PluginList", {}),
        ("IsFileBeingDebugged", {}), ("CurrentExceptionNumber", {}),
        ("GetJIT", {}), ("GetCommandLine", {}), ("ShowBreakPoint", {}),
        ("GetExceptionSettings", {}), ("HistoryClear", {}), ("TraceClearLog", {}),
        ("WatchdogClear", {}), ("ClearLog", {}),
    ]
    t0 = time.time()
    fails = 0
    for i in range(300):
        iface, p = random.choice(pool)
        st, d, e = post(iface, p, timeout=10)
        if st < 0 or e or d is None:
            fails += 1
            if fails <= 5:
                print("  长序列失败: %s st=%s e=%s" % (iface, st, e))
    dt = time.time() - t0
    alive, st, err = service_alive()
    check(alive, "300 命令长序列后服务存活")
    check(fails == 0, "300 命令长序列全部成功 (%d fails, %.1fs)" % (fails, dt))

# ---------------- P4 无效输入压力 ----------------
def stress_invalid():
    cases = [
        ("非法JSON", "this is not json"),
        ("数组JSON", "[1,2,3]"),
        ("空体", ""),
        ("超长params(截断)", json.dumps({"interface": "ProcessList", "params": "A" * 500000})),
        ("不存在接口", json.dumps({"interface": "NoSuchInterface_XYZ", "params": ""})),
        ("空interface", json.dumps({"interface": "", "params": ""})),
        ("interface为数字", json.dumps({"interface": 123, "params": ""})),
        ("超长命令串", json.dumps({"interface": "ExecuteCommand", "params": "SetLabel --address 0x401000 --name " + "X" * 100000})),
        ("深层JSON", '{"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":{"i":{"j":{"k":1}}}}}}}}}}}'),
    ]
    fails = 0
    for name, payload in cases:
        st, d, e = post_raw(payload, timeout=15)
        if st == -1 or (st >= 500):
            fails += 1
            check(False, "无效输入[%s] 服务异常: st=%s e=%s" % (name, st, e))
        else:
            check(True, "无效输入[%s] 优雅处理 st=%s" % (name, st))
    # 超时压力: 慢命令 (SysCommand sleep) 不应挂死服务
    st, d, e = post("SysCommand", {"shell": "cmd", "command": "ping -n 2 127.0.0.1 > nul", "timeout": 5}, timeout=15)
    res = expect_ok(st, d, e, "SysCommand(慢)", "")
    if res: check(res.get("ok") == 1 or res.get("ok") == 0, "SysCommand 慢命令返回")
    alive, st, err = service_alive()
    check(alive, "无效输入后服务存活 (fails=%d)" % fails)

# ---------------- P5 资源泄漏对比 ----------------
def _proc_stats():
    try:
        out = subprocess.run(
            ["powershell", "-NoProfile", "-Command",
             "Get-Process BinSentry -ErrorAction SilentlyContinue | Select-Object Id,WorkingSet64,Handles,PrivateMemorySize64 | ConvertTo-Json"],
            capture_output=True, text=True, timeout=15).stdout.strip()
        if not out:
            return None
        data = json.loads(out)
        if isinstance(data, list):
            data = data[0]
        return data
    except Exception:
        return None

def stress_leak():
    before = _proc_stats()
    # 大量会话命令(有状态)循环
    for i in range(6):
        st, d, e = post("Debug", {"path": WIN32}, timeout=10)
        expect_ok(st, d, e, "Debug(leak)", "")
        time.sleep(0.4)
        st, d, e = post("WaitStop", 3000, timeout=10)
        expect_ok(st, d, e, "WaitStop(leak)", "")
        st, d, e = post("StepIn", "1", timeout=8); time.sleep(0.2)
        st, d, e = post("Register", {}, timeout=8)
        expect_ok(st, d, e, "Register(leak)", "")
        st, d, e = post("Detach", {}, timeout=8)
        expect_ok(st, d, e, "Detach(leak)", "")
        time.sleep(0.2)
    time.sleep(1)
    after = _proc_stats()
    if not before or not after:
        check(False, "无法获取进程资源统计")
        return
    dws = int(after.get("WorkingSet64", 0)) - int(before.get("WorkingSet64", 0))
    dpm = int(after.get("PrivateMemorySize64", 0)) - int(before.get("PrivateMemorySize64", 0))
    dh = int(after.get("Handles", 0)) - int(before.get("Handles", 0))
    # 6 轮会话, 内存/句柄增长超过阈值视为潜在泄漏
    check(dws < 8 * 1024 * 1024, "WorkingSet 增长 %.2fMB (阈值 8MB)" % (dws / 1048576.0))
    check(dpm < 8 * 1024 * 1024, "PrivateMemory 增长 %.2fMB (阈值 8MB)" % (dpm / 1048576.0))
    check(abs(dh) < 200, "Handles 变化 %d (阈值 200)" % dh)

# ---------------- 主流程 ----------------
def main():
    if not start_service():
        print("[!] 无法启动服务, 退出")
        return 1
    cleanup_targets()
    sections = []
    if not STRESS_ONLY:
        sections += [
            ("S1 全接口边界冒烟", smoke_all_interfaces),
            ("S2 无会话白名单实测", smoke_whitelist),
            ("S5 文件冒烟(无会话)", smoke_files),
            ("S6 全接口稳定性", smoke_stability_all),
            ("S3 会话冒烟(win32.exe)", smoke_session),
            ("S4 符号冒烟(_symtest.exe)", smoke_symbols),
        ]
    if not SMOKE_ONLY:
        sections += [
            ("P1 并发压力(16x50)", stress_concurrent),
            ("P2 连续调试压力(20轮)", stress_debug_loop),
            ("P3 长序列压力(300命令)", stress_long_sequence),
            ("P4 无效输入压力", stress_invalid),
            ("P5 资源泄漏对比", stress_leak),
        ]
    for name, fn in sections:
        print("\n== %s ==" % name)
        run_case(name, fn)
    # 汇总
    total = PASS + FAIL + ERROR
    print("\n" + "=" * 60)
    print("汇总: PASS=%d FAIL=%d ERROR=%d 合计断言=%d" % (PASS, FAIL, ERROR, total))
    print("=" * 60)
    # 失败明细
    bad = [d for d in DETAILS if d[0] != "PASS"]
    if bad:
        print("\n失败/错误明细:")
        for tag, msg in bad:
            print("  [%s] %s" % (tag, msg))
    if not KEEP:
        stop_service()
    cleanup_targets()
    return 0 if FAIL == 0 and ERROR == 0 else 1

if __name__ == "__main__":
    sys.exit(main())
posted @ 2026-09-26 13:06  lyshark  阅读(12)  评论(0)    收藏  举报