chore(vet): 初始化 VET 仓(Viper Environment Tool)

首次入库,远端 https://git.gvsds.com/GVSDS/Viper_VET 。

19 个文件全部为源码/配置,无构建产物:
  App/main.vp、App/lib/*.vp(VCache / VCheck / VCli / VDirs / VFs / VGuard /
    VGuid / VJson / VLog / VPe / VProc / VScan / __init__)
  1.json、LICENSE、README.md、project.vpj、.gitignore

既有 .gitignore 已覆盖本仓的产物目录(**/output/、**/temp/、**/.tpv_build/)
与 *.exe / *.obj / *.o / *.img / *.iso / __pycache__/ / .cache/;
实测 `git add -A --dry-run` 仅 19 个文件、零产物混入,故未改动 .gitignore。
This commit is contained in:
TermiNexus committed 2026-09-29 15:20:13 +08:00
commit 02df100204
19 files changed
+6160

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
*.o
*.obj
*.exe
*.img
*.iso
**/__pycache__/
.cache/
*.pyc
# 编译
**/output/
**/temp/
**/.tpv_build/
+1
View File
@@ -0,0 +1 @@
[{"id":"80265108-c9b5-4d99-a28b-74031104ffe3","executable":"D:\\Users\\TermiNexus\\Desktop\\TransPyC\\TransPyV\\output\\TransPyV.exe","version":"0.0.3-dev.428","arch":"x86_64","source":"workspace","displayName":"Viper 0.0.3-dev.428"}]
+354
View File
@@ -0,0 +1,354 @@
import t, c
from stdint import *
import stdlib
import string
import memhub
import json
import lib.VFs as VFs
import lib.VLog as VLog
import w32.win32file as wf
import w32.win32base as wb
# ============================================================
# VCache - 发现缓存(内存态 + 缓存文件读写/解析/失效校验)
#
# 从原 App/main.vp 平移 Cache*/LoadCache/WriteCacheFile/ParseCacheText/ValidateCache。
# 与 VJson 解耦:WriteCacheFile 不再直接读 VJson 的 Json_Buf,而是由调用方把
# (Buf, Len)传入;表项读取经 CacheCount()/CacheExe(i)… 暴露,避免 VJson↔VCache 环。
#
# 日志详细度:缓存载入/逐条移除明细("已载入 N 项种子"、"[移除] 已不存在: <路径>"、
# "未找到/无法打开: <路径>")属过程细节,走 VLog.Verbose*;解析/读取失败等降级说明
# (文本超限、工作池预检不足、JSON 解析失败、写入失败)始终输出且不内嵌路径。
#
# 本轮改造(需求 ②):解析不再用自写的"极小 JSON 状态机"(原 ReadJsonStr +
# 逐字符扫描),改为调用仓库 json 库 json.parse(includes/json/__parser.vp:286),
# 再按字段搬进 C 侧缓存表。内存归属见 ParseCacheText 注释。
#
# 依赖:VFs(DupStr/PathIsFile/ReadWholeFile/WriteFileAll/Utf8ToAnsi/CopyRaw)、
# VLog、json(parse/JsonValue/JSON_* 常量)、stdlib(malloc/free)。
# 字段/容量沿用原实现(App/main.vp 拆分前 :140-150、:188-194)。
# ============================================================
CACHE_CAP: t.CDefine = 256 # 缓存条目上限
CACHE_FILE_MAX: t.CDefine = 16777216 # 缓存文件读取上限 16 MiB
DISP_CAP: t.CDefine = 160 # "Viper " + 版本串
CACHE_TEXT_MAX: t.CDefine = 262144 # 解析文本上限 256 KiB(典型缓存约 60 KiB;超出即视为损坏/异常,按空缓存启动)
CACHE_ARENA_PER_BYTE: t.CDefine = 128 # 解析工作池容量 / 输入字节(保守上界,见 ParseCacheText)
CACHE_ARENA_SLACK: t.CDefine = 4096 # 解析工作池固定余量
# 由 App/main.vp 注入
_mbuddy: rawptr[memhub.MemBuddy]
# 发现缓存(内存态;ANSI 路径,用于与 FindFirstFileA 枚举结果直接比对)
# Cache_Src 记录条目来源("workspace" = 扫盘发现;"check" = check 通道命中),
# 是 JSON 条目 source 字段的唯一来源(VJson.BuildEntry 读 CacheSrc)。
Cache_Exe: t.CArray[str, CACHE_CAP]
Cache_Id: t.CArray[str, CACHE_CAP]
Cache_Ver: t.CArray[str, CACHE_CAP]
Cache_Arch: t.CArray[str, CACHE_CAP]
Cache_Src: t.CArray[str, CACHE_CAP]
Cache_Disp: t.CArray[str, CACHE_CAP]
Cache_Count: int = 0
# source 字面量:扫盘侧(VScan)/ check 侧(VCheck)各自传入,缓存本身不解释其含义。
# 旧缓存文件(无 source 字段)按 SOURCE_FALLBACK 载入,保持既有输出不变。
SOURCE_FALLBACK: t.CDefine = "workspace"
# ---- 表读取接口(供 VJson 构建 JSON,不暴露数组本身) ----
def CacheCount() -> int:
return Cache_Count
def CacheExe(Idx: int) -> str:
return Cache_Exe[Idx]
def CacheId(Idx: int) -> str:
return Cache_Id[Idx]
def CacheVer(Idx: int) -> str:
return Cache_Ver[Idx]
def CacheArch(Idx: int) -> str:
return Cache_Arch[Idx]
def CacheSrc(Idx: int) -> str:
return Cache_Src[Idx]
def CacheDisp(Idx: int) -> str:
return Cache_Disp[Idx]
# ============================================================
# 内存缓存
# ============================================================
def CacheFind(ExeAnsi: str) -> int:
i: int = 0
while i < Cache_Count:
if string.strcmp(Cache_Exe[i], ExeAnsi) == 0:
return i
i += 1
return -1
def FreeEntry(Idx: int):
_mbuddy.free(t.CPtr(Cache_Exe[Idx]))
_mbuddy.free(t.CPtr(Cache_Id[Idx]))
_mbuddy.free(t.CPtr(Cache_Ver[Idx]))
_mbuddy.free(t.CPtr(Cache_Arch[Idx]))
_mbuddy.free(t.CPtr(Cache_Src[Idx]))
_mbuddy.free(t.CPtr(Cache_Disp[Idx]))
# 移除第 Idx 条(释放各字段副本并把后续条目左移一位)。返回 1=已移除。
# 单点实现:ValidateCache(存在性校验)与 VScan 的"每轮完整重校"都经此处剔除条目,
# 避免两处各写一遍左移逻辑。
def RemoveAt(Idx: int) -> int:
global Cache_Count
if Idx < 0 or Idx >= Cache_Count:
return 0
FreeEntry(Idx)
k: int = Idx
while k < Cache_Count - 1:
Cache_Exe[k] = Cache_Exe[k + 1]
Cache_Id[k] = Cache_Id[k + 1]
Cache_Ver[k] = Cache_Ver[k + 1]
Cache_Arch[k] = Cache_Arch[k + 1]
Cache_Src[k] = Cache_Src[k + 1]
Cache_Disp[k] = Cache_Disp[k + 1]
k += 1
Cache_Count -= 1
return 1
# 清空内存表(释放全部字段副本)。用于每轮开始前的"重读 cache.json"。
def ClearCache():
global Cache_Count
j: int = 0
while j < Cache_Count:
FreeEntry(j)
j += 1
Cache_Count = 0
# 把一条发现记录拷入缓存(各字段独立副本)。返回 1=成功,0=容量满/分配失败
# Src:条目来源("workspace"/"check"),原样存入并在 JSON 输出时透传。
def CacheAdd(ExeAnsi: str, Id: str, Ver: str, Arch: str, Src: str, Disp: str) -> int:
global Cache_Count
if Cache_Count >= CACHE_CAP:
VLog.LogFmt1(" 警告: 缓存条目已达上限 %d,忽略新条目\n", t.CSizeT(CACHE_CAP))
return 0
E: str = VFs.DupStr(ExeAnsi)
if E is None:
return 0
I: str = VFs.DupStr(Id)
if I is None:
_mbuddy.free(t.CPtr(E))
return 0
V: str = VFs.DupStr(Ver)
if V is None:
_mbuddy.free(t.CPtr(E))
_mbuddy.free(t.CPtr(I))
return 0
A: str = VFs.DupStr(Arch)
if A is None:
_mbuddy.free(t.CPtr(E))
_mbuddy.free(t.CPtr(I))
_mbuddy.free(t.CPtr(V))
return 0
S: str = VFs.DupStr(Src)
if S is None:
_mbuddy.free(t.CPtr(E))
_mbuddy.free(t.CPtr(I))
_mbuddy.free(t.CPtr(V))
_mbuddy.free(t.CPtr(A))
return 0
D: str = VFs.DupStr(Disp)
if D is None:
_mbuddy.free(t.CPtr(E))
_mbuddy.free(t.CPtr(I))
_mbuddy.free(t.CPtr(V))
_mbuddy.free(t.CPtr(A))
_mbuddy.free(t.CPtr(S))
return 0
Cache_Exe[Cache_Count] = E
Cache_Id[Cache_Count] = I
Cache_Ver[Cache_Count] = V
Cache_Arch[Cache_Count] = A
Cache_Src[Cache_Count] = S
Cache_Disp[Cache_Count] = D
Cache_Count += 1
return 1
# 校验缓存项对应的 exe 是否仍存在:不存在则从缓存移除(返回移除数)
def ValidateCache() -> int:
removed: int = 0
j: int = 0
while j < Cache_Count:
if VFs.PathIsFile(Cache_Exe[j]) != 0:
j += 1
else:
VLog.VerboseFmtS1(" [移除] 已不存在: %s\n", Cache_Exe[j])
RemoveAt(j)
removed += 1
return removed
# ============================================================
# 缓存文件
# ============================================================
# 把 EmitJson 生成的文本(Buf,共 Len 字节)写入缓存文件(失败只告警,不中断进程)
def WriteCacheFile(Path: str, Buf: bytes, Len: t.CSizeT) -> int:
if Buf is None:
return 0
rc: int = VFs.WriteFileAll(Path, Buf, Len)
if rc == 0:
VLog.LogFmt1(" 警告: 缓存写入失败(打开失败, win32 err=%u)\n", wb.GetLastError())
return 0
if rc < 0:
VLog.LogFmt2(" 警告: 缓存写入不完整 (%zu/%zu 字节)\n", VFs.LastWriteDone, Len)
return 0
return 1
# 取对象里指定键的字符串值;键缺失/非字符串/值为空指针 ⇒ 空串
# (与原实现"缺字段记 ''"一致)。纯只读字段遍历,先例见 includes/_dict.vp:241-261。
def FieldStr(Obj: rawptr[json.JsonValue], Key: str) -> str:
cu: rawptr[json.JsonValue] = Obj.child
while cu != None:
if cu.key != None:
if string.strcmp(cu.key, Key) == 0:
if cu.vtype == json.JSON_STRING and cu.str_val != None:
return cu.str_val
return ""
cu = cu.next
return ""
# 把一个解析出的 JSON 对象搬进缓存表(逐字段取出 → CacheAdd 在 Vet_Mb 上复制持久副本)。
# 需求 executable 与 id 非空才计入(与原实现 "TmpExe[0]!=0 and TmpId[0]!=0" 等价)。
# source 缺失/非字符串/空串 ⇒ SOURCE_FALLBACK(旧缓存文件无该字段,输出与既有行为一致)。
def AddParsedEntry(Obj: rawptr[json.JsonValue], ConvBuf: bytes) -> int:
Exe: str = FieldStr(Obj, "executable")
Id: str = FieldStr(Obj, "id")
if string.strlen(Exe) == 0 or string.strlen(Id) == 0:
return 0
Src: str = FieldStr(Obj, "source")
if string.strlen(Src) == 0:
Src = SOURCE_FALLBACK
# 与扫描侧口径一致:缓存内 executable 存 ANSI(CacheFind 要与 FindFirstFileA 的 ANSI 结果比对)
if VFs.Utf8ToAnsi(Exe, ConvBuf, VFs.CONV_CAP) == 0:
VFs.CopyRaw(ConvBuf, VFs.CONV_CAP, Exe)
return CacheAdd(str(ConvBuf), Id, FieldStr(Obj, "version"),
FieldStr(Obj, "arch"), Src, FieldStr(Obj, "displayName"))
# 用仓库 json 库解析缓存文本(顶层数组 + 对象 + 字符串字段),逐字段搬入 C 侧缓存表。
# 返回成功计入的条目数。
#
# 内存归属(避免悬空/双重释放):
# * json.parse 的整棵树(JsonValue 节点 + 键名副本 + 字符串副本)全部分配在**本次调用
# 专用的 MemPool**(bump 分配器)上,arena 由 stdlib.malloc 单独提供;
# * 树内的字符串只在 CacheAdd 期间被读取,CacheAdd 用 VFs.DupStr 在 Vet_Mb(_mbuddy)
# 上另做持久副本 —— 两池互不相干;
# * 因此解析结束后**整块** stdlib.free(arena) 即回收全部解析内存,既不悬空也不会
# 误放缓存表项(这正是不复用 Vet_Mb 的原因:Vet_Mb 上 CacheAdd 的分配与解析树的
# 分配交错,无法用 mark/reset_to 只回收解析树)。
#
# 失败处理(均非致命,告警后按空缓存启动):
# * 文本为空 / 超长(> CACHE_TEXT_MAX)/ arena 分配失败 / 工作池容量预检不足
# (Pool.AllocOk(Need) 失败 ⇒ 直接降级,见该处注释)/ 顶层非数组 / 解析出 null。
def ParseCacheText(Txt: str) -> int:
if Txt is None:
return 0
L: t.CSizeT = string.strlen(Txt)
if L == 0:
return 0
if L > t.CSizeT(CACHE_TEXT_MAX):
VLog.LogFmt2("缓存: 文本 %zu 字节超出解析上限 %zu,按空缓存启动\n",
L, t.CSizeT(CACHE_TEXT_MAX))
return 0
# 工作池容量:解析树最坏 ≈ 每输入字节 1 个 JsonValue 节点(9 字段 × 8 = 72 字节上界,bump 池
# 再按 8 字节对齐)+ 每字符串一次键名副本与值副本 + 数字/键名临时缓冲(最坏 ≈ 80 字节/输入字节);
# 按 CACHE_ARENA_PER_BYTE(128)再放大,覆盖每次分配的 8 字节对齐浪费。
# (MemPool 耗尽会 printf + exit(1),故必须一次给足;这是不选 MemPool 小池的原因。)
Cap: t.CSizeT = L * t.CSizeT(CACHE_ARENA_PER_BYTE) + t.CSizeT(CACHE_ARENA_SLACK)
Arena: bytes = stdlib.malloc(Cap)
if Arena is None:
VLog.LogStr("缓存: 解析工作池分配失败,按空缓存启动\n")
return 0
Pool: rawptr[memhub.MemPool] = memhub.MemPool(Arena, Cap)
if Pool is None:
stdlib.free(t.CPtr(Arena))
VLog.LogStr("缓存: 解析工作池构造失败,按空缓存启动\n")
return 0
# 池容量预检(fail-closed):整棵解析树必须先确认放得下,否则 json.parse 内部的
# pool.alloc 会走到 includes/memhub.vp:262-272 的 printf + exit(1)。
# Need 与 Cap 同源(同一 L、同一每字节放大系数与固定余量),且 L*128 与 4096 都是
# MEMHUB_ALIGN(8) 的倍数 ⇒ 新池 offset=0 时 AllocOk(Need) 恒成立;一旦 Cap 的推导被
# 改小到放不下最坏解析开销,此处即失败并按"空缓存启动"降级,不会再走到 exit(1)。
Need: t.CSizeT = L * t.CSizeT(CACHE_ARENA_PER_BYTE) + t.CSizeT(CACHE_ARENA_SLACK)
if Pool.AllocOk(Need) == 0:
stdlib.free(t.CPtr(Arena))
VLog.LogFmt1("缓存: 解析工作池预检不足(最坏需要 %zu 字节),按空缓存启动\n",
Need)
return 0
ConvBuf: bytes = _mbuddy.alloc(VFs.CONV_CAP)
if ConvBuf is None:
stdlib.free(t.CPtr(Arena))
VLog.LogStr("缓存: 编码转换缓冲分配失败,按空缓存启动\n")
return 0
Root: rawptr[json.JsonValue] = json.parse(Pool, Txt)
added: int = 0
if Root == None:
VLog.LogStr("缓存: JSON 解析失败(内容损坏),按空缓存启动\n")
elif Root.vtype != json.JSON_ARRAY:
VLog.LogStr("缓存: 顶层不是 JSON 数组,按空缓存启动\n")
else:
cu: rawptr[json.JsonValue] = Root.child
while cu != None:
if cu.vtype == json.JSON_OBJECT:
added += AddParsedEntry(cu, ConvBuf)
cu = cu.next
_mbuddy.free(t.CPtr(ConvBuf))
stdlib.free(t.CPtr(Arena))
return added
# 启动时读缓存做种子。文件不存在/读失败/解析失败均为非致命:告警后按空缓存启动。
def LoadCache(Path: str) -> int:
buf: bytes = VFs.ReadWholeFile(Path, t.CSizeT(CACHE_FILE_MAX),
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE)
if buf is None:
rc_err: int = VFs.LastReadErr
if rc_err == 1:
VLog.VerboseFmtS1("缓存: 未找到/无法打开,按空缓存启动: %s\n", Path)
elif rc_err == 2:
VLog.LogStr("缓存: 读取大小失败,按空缓存启动\n")
elif rc_err == 5:
VLog.LogStr("缓存: 分配缓冲失败,按空缓存启动\n")
else:
VLog.LogStr("缓存: 大小异常/过大,按空缓存启动\n")
return 0
n: int = ParseCacheText(str(buf))
stdlib.free(t.CPtr(buf))
VLog.VerboseFmt1("缓存: 已载入 %u 项种子\n", t.CSizeT(n))
return n
# 每轮开始前的"重读 cache.json":先清空内存表(释放上一轮的字段副本),再按文件重建。
# 返回本次载入的条目数。文件不存在/读取失败/解析失败均按空缓存继续(与 LoadCache 同一口径,
# 由 LoadCache 内部告警);代价是"上一轮写盘失败时进程内已知条目会丢失"——为此把写盘失败
# 只降级为告警(WriteCacheFile),并由"下一轮树扫描会重新发现"兜底(扫盘幂等)。
def ReloadCache(Path: str) -> int:
ClearCache()
return LoadCache(Path)
+546
View File
@@ -0,0 +1,546 @@
import t, c
from stdint import *
import stdlib
import string
import viperlib
import memhub
import json
import lib.VFs as VFs
import lib.VLog as VLog
import lib.VPe as VPe
import lib.VProc as VProc
import lib.VGuid as VGuid
import lib.VCache as VCache
# ============================================================
# VCheck - check 请求通道的"执行 + 回执"侧(父进程 / scan 进程内)
#
# 通道形态(依附 --need-confirm:只有开了心跳、看门狗存在时才具备该通道):
# * 用户/外部往 **stdin** 写一行请求:
# {"check": "<exe 绝对路径>", "id": <uint64>}
# * 看门狗(confirm-watch)用 json 库解析该行:含 "check"(字符串) 且含 "id"(整数)
# ⇒ 判定为 check 请求,原样逐字节转发(含 '\n')到 IPC 管道写端;该行不参与心跳判定。
# * 父进程(scan)在**非阻塞**轮询点读该管道(见 VGuard.PollIpc),把完整一行交给本模块。
# * 本模块执行检查并往 **stdout** 写**单行 JSON** 回执:
# 成功 {"check_result": true, "id": <id>, "viper": {…发现项…}} (不含 check_msg)
# 失败 {"check_result": false, "id": <id>, "check_msg": "<原因>"}
#
# 回执由仓库 json 库序列化(json.object_set + json.write(pool, obj, False)):
# 紧凑单行、转义与 UTF-8 透传由库保证、键序 = 插入序(见 includes/json/**)。
#
# 检查内容(与扫盘候选过滤逐条同口径,口径单点来源):
# ① PE/.PRVERSN:VPe.TryReadPeInfo(打开失败的错误码经 VPe.PeOpenErr 区分 NOT_FOUND/NO_PERMISSION)
# ② --version :VProc.RunVersionProbe(超时 / 拉起失败的错误码 / 前 5 字符为 "Viper")
#
# 失败原因**只允许**以下固定英文字符串(大小写严格):
# NOT_FOUND 未找到文件(CreateFileA 失败且 GetLastError ∈ {2,3})
# NO_PERMISSION 无权限(GetLastError ∈ {5,32},以及其余一切打开失败取值)
# NOT_EXE 非 PE(文件过短/读不出头部/MZ 缺失/e_lfanew 越界/PE 签名缺失/可选头 magic 非法)
# NO_PRVERSN 缺失 .PRVERSN 段
# PRVERSN_ERR .PRVERSN 段异常(段定位/读取失败、段内容为空、段内容非法)
# PREFIX_ERR --version 拉起成功但输出前 5 字符不是 Viper
# EXE_ERR,0x<HEX> 拉起 <exe> --version 失败,逗号 + 十六进制错误码(大写十六进制,与 VPe
# 的 "unknown(0x%X)" 同一写法;取值来自 VProc.ProbeLaunchErr)
# TIMEOUT --version 探测超时(VProc.Probe_TimedOut)
# CHANNEL_UNAVAILABLE 本模块的缓冲/回执池不可用(CheckInit 失败)⇒ 无法执行检查,
# 只给出"通道不可用"这一明确原因(降级回执,见 EmitReplyUnavailable)
#
# 优先级:check 比普通扫盘高。识别到请求后本模块**同步**完成"读 .PRVERSN → 执行 --version →
# 回执"整段,期间树扫描停在 VScan 的轮询点(PaceDir/PaceFile/探测前/轮首末)上不前进;
# 处理完 PollIpc 才返回扫盘循环。单线程串行 ⇒ 不存在"check 与树扫描交错"的窗口。
#
# 每轮完整重校:VScan.RevalidateDiscovered 复用本模块的 VerifyExecutable(同一口径的
# "文件存在 + PE/.PRVERSN 可解析 + --version 前 5 字符为 Viper"),不合格的从缓存剔除。
#
# 内存归属:
# * 缓冲与回执工作池在 CheckInit 内一次性建立(_mbuddy 的字段缓冲 + stdlib 的池 arena),
# 进程生命周期内复用;回执每条先 Check_Pool.reset()(池 offset 归零)再构造;
# * ParseCheckLine 的解析树放在**本次调用专用**的 stdlib arena 上,返回前整块 free:
# 与缓存表(_mbuddy 上 CacheAdd 的持久副本)互不相干,不会悬空/双重释放
# (与 VCache.ParseCacheText 同一手法与理由)。
#
# 降级(fail-safe 但不静默关闭功能):
# * CheckInit 失败(缓冲/回执池分配失败)时,HandleCheckLine 仍会**识别请求并回执**一行
# check_result=false + check_msg="CHANNEL_UNAVAILABLE":该行不依赖 _mbuddy/Check_Pool
# (用 stdlib 临时缓冲 + 固定模板直接产出,见 EmitReplyUnavailable),因此"请求已收到、
# 但通道不可用"这一事实对请求方始终可见;只有连该临时缓冲都拿不到时才彻底不回复;
# * 每轮重校(VerifyExecutable)在 CheckInit 失败时返回 1 = 跳过重校、不剔除任何条目
# (绝不把整表误判为不合格清掉)。该降级在扫盘启动时由 VScan.ScanInit 的告警一次性说明。
#
# 注入前提:本模块的 _mbuddy 必须由 App/main.vp 与其它模块同一 MemBuddy 实例注入;未注入时
# _mbuddy 指向零值静态存储(usable == NULL)⇒ 全部 alloc 返回 None ⇒ CheckInit 必失败。
#
# 依赖本仓既有:VFs.Utf8ToAnsi/AnsiToUtf8/CopyRaw、VLog.*、VPe.TryReadPeInfo、
# VProc.RunVersionProbe/VERSION_PROBE_DIAG_CAP、VGuid.NewGuidStr、VCache.CacheFind/
# CacheAdd/CacheId/CacheVer/CacheArch/CacheDisp、json.parse/object/object_set/
# string_val/int_val/bool_val/write、memhub.MemPool/NewMemPool、stdlib.malloc/free。
# ============================================================
# 固定失败原因(check_msg 的全部取值;EXE_ERR 是前缀 + 十六进制码,见 FormatExeErr)
CK_NOT_FOUND: t.CDefine = "NOT_FOUND"
CK_NO_PERMISSION: t.CDefine = "NO_PERMISSION"
CK_NOT_EXE: t.CDefine = "NOT_EXE"
CK_NO_PRVERSN: t.CDefine = "NO_PRVERSN"
CK_PRVERSN_ERR: t.CDefine = "PRVERSN_ERR"
CK_PREFIX_ERR: t.CDefine = "PREFIX_ERR" # --version 拉起成功但输出前 5 字符不是 Viper
CK_TIMEOUT: t.CDefine = "TIMEOUT"
# 通道自身不可用(CheckInit 失败):降级回执的唯一原因串,见 EmitReplyUnavailable
CK_CHANNEL_UNAVAILABLE: t.CDefine = "CHANNEL_UNAVAILABLE"
# check 命中入库时的 source 值(与扫盘侧 "workspace" 区分)
CK_SOURCE: t.CDefine = "check"
# EXE_ERR 的原因串缓冲("EXE_ERR,0x" + 最多 8 位十六进制 + NUL)
CK_MSG_CAP: t.CDefine = 32
# 请求行解析上限(与 VGuard.GUARD_LINE_CAP 对齐:看门狗的行缓冲即本上限,
# 超长行在看门狗侧已被截断/丢弃,这里再兜一层,避免对异常长的输入做解析)
CK_LINE_MAX: t.CDefine = 4096
# 解析工作池容量 / 输入字节与固定余量:与 VCache.ParseCacheText 同一保守口径
# (最坏 ≈ 每输入字节 1 个 JsonValue 节点 + 键名/值副本 + 数字临时缓冲 ≈ 80 字节/输入字节,
# 再按 128 放大覆盖 8 字节对齐浪费)。MemPool 耗尽会 exit(1),故必须先 AllocOk 预检。
CK_ARENA_PER_BYTE: t.CDefine = 128
CK_ARENA_SLACK: t.CDefine = 4096
# 回执工作池:72 KiB。单条回执的全部池分配(Need)见 CK_REPLY_NEED_FIXED 的推导。
CK_REPLY_POOL_CAP: t.CDefine = 73728
# 单条回执在回执池上的固定开销上界:
# * JsonValue 节点 9 个(回执对象 + check_result + id + viper 子对象 + 6 个字符串值)
# × 单节点上界 72 字节 = 648(JsonValue 9 字段 × 8 字节,includes/json/__init__.vp:23-39);
# * 键名副本(含 NUL)8 个:check_result(13) + id(3) + viper(6) + id(3) + executable(11)
# + version(8) + arch(5) + source(7) + displayName(12) = 68;
# * json.write 的输出缓冲 4096(includes/json/__writer.vp:143-151 的缺省 cap);
# * 整数序列化的临时缓冲 32 字节(write 的 JSON_INT 分支 pool.alloc(32));
# * 余量 512:各值副本的结尾 NUL 与按 MEMHUB_ALIGN(8) 向上取整的浪费。
# 合计 648 + 68 + 4096 + 32 + 512 = 5356 ⇒ 取 5632(512 对齐)。
CK_REPLY_NEED_FIXED: t.CDefine = 5632
# 回执里 executable / displayName 的取值长度上界:二者都经 Check_Conv(VFs.CONV_CAP 字节)
# 转出(WideCharToMultiByte 以 Cap 封顶并写结尾 NUL,失败时 CopyRaw 写空串)⇒ ≤ CONV_CAP-1。
# 写 2047 = VFs.CONV_CAP(2048) - 1 的字面量(与本仓"t.CDefine 不引用跨模块常量"的惯例一致)。
CK_CONV_MAX: t.CDefine = 2047
# 降级回执(EmitReplyUnavailable)的临时行缓冲字节数:stdlib.malloc,与 _mbuddy 无关,
# 因为该路径的定义就是"mb 上的缓冲拿不到"。长度上界(含换行与结尾 NUL):
# `{"check_result":false,"id":` 27 + id 最长 20(i64 十进制含负号)
# + `,"check_msg":"` 14 + CHANNEL_UNAVAILABLE 19 + `"}` 2 + '\n' 1 + NUL 1 = 84 ⇒ 取 128。
CK_FALLBACK_CAP: t.CDefine = 128
# 由 App/main.vp 注入(与其它模块同一 MemBuddy 实例)
_mbuddy: rawptr[memhub.MemBuddy]
# 一次性初始化的状态(Check_Tried 保证"初始化失败"不会每次调用都重试一遍分配)
Check_Tried: int = 0
Check_Ready: int = 0
Check_Handling: int = 0 # 1=正在处理一条 check(防重入;PollIpc 由扫盘多处调用)
Check_Ver: bytes = None # .PRVERSN 版本串(VPe.VER_CAP)
Check_Arch: bytes = None # arch 字符串(VPe.ARCH_CAP)
Check_Probe: bytes = None # --version 捕获缓冲(VProc.VERSION_PROBE_DIAG_CAP)
Check_Msg: bytes = None # EXE_ERR 原因串缓冲(CK_MSG_CAP)
Check_Path: bytes = None # 请求里的 exe 路径(ANSI;VFs.PATH_BUF_CAP)
Check_Id: bytes = None # 新条目的 UUID(VGuid.GUID_STR_CAP)
Check_Disp: bytes = None # "Viper " + 版本串(VCache.DISP_CAP)
Check_Conv: bytes = None # ANSI → UTF-8 转换缓冲(VFs.CONV_CAP)
Check_Arena: bytes = None # 回执工作池 arena(stdlib.malloc,常驻)
Check_Pool: rawptr[memhub.MemPool] = None # 回执工作池(堆上就地构造,条目级 reset 复用)
# ParseCheckLine 的结果(模块全局出参,与 VPe.Pe_RawPtr 同一惯例)
Req_Id: LONGLONG = 0 # 请求里的 id(json 库 int64 域;回执原样带回)
Req_PathOk: int = 0 # 1=Check_Path 已填(可执行检查);0=请求已识别但路径缓冲不可用
# ============================================================
# 初始化
# ============================================================
# 请求路径缓冲(请求行解析与 CheckInit 共用)。看门狗进程只做"分类转发",不跑 CheckInit,
# 但它也要解析请求行(取出路径)⇒ 该缓冲由最先用到的一方按需分配,两边都不重复分配。
def EnsurePathBuf() -> int:
global Check_Path
if Check_Path is None:
Check_Path = _mbuddy.alloc(VFs.PATH_BUF_CAP)
if Check_Path is None:
return 0
return 1
# 分配检查缓冲与回执工作池(幂等;失败只告警一次并由 Check_Ready 标记为不可用)。
# 失败语义(fail-safe):check 请求回执 CHANNEL_UNAVAILABLE(见 EmitReplyUnavailable);
# 每轮重校直接跳过(不剔除任何条目)——绝不因"缓冲分配失败"而把整张缓存表误判为不合格清掉。
# 告警按"哪一组分配失败"区分:四组缓冲来源不同(_mbuddy 上的字段缓冲 / stdlib 的池 arena),
# 同一句文案会让人无法判断是哪一处(历史现场:_mbuddy 未注入时四组全失败,只能看到同一句)。
def CheckInit() -> int:
global Check_Tried, Check_Ready
global Check_Ver, Check_Arch, Check_Probe, Check_Msg
global Check_Id, Check_Disp, Check_Conv, Check_Arena, Check_Pool
if Check_Tried != 0:
return Check_Ready
Check_Tried = 1
Check_Ver = _mbuddy.alloc(VPe.VER_CAP)
Check_Arch = _mbuddy.alloc(VPe.ARCH_CAP)
Check_Probe = _mbuddy.alloc(VProc.VERSION_PROBE_DIAG_CAP)
Check_Msg = _mbuddy.alloc(CK_MSG_CAP)
Check_Id = _mbuddy.alloc(VGuid.GUID_STR_CAP)
Check_Disp = _mbuddy.alloc(VCache.DISP_CAP)
Check_Conv = _mbuddy.alloc(VFs.CONV_CAP)
Check_Arena = stdlib.malloc(CK_REPLY_POOL_CAP)
if EnsurePathBuf() == 0:
VLog.LogStr(" 警告: check 缓冲分配失败(请求路径缓冲),check 通道不可用\n")
return 0
if Check_Ver is None or Check_Arch is None or Check_Probe is None:
VLog.LogStr(" 警告: check 缓冲分配失败(版本/arch/探测缓冲),check 通道不可用\n")
return 0
if Check_Msg is None or Check_Id is None:
VLog.LogStr(" 警告: check 缓冲分配失败(原因串/UUID 缓冲),check 通道不可用\n")
return 0
if Check_Disp is None or Check_Conv is None or Check_Arena is None:
VLog.LogStr(" 警告: check 缓冲分配失败(展示串/转换缓冲/回执 arena),check 通道不可用\n")
return 0
# 池对象走堆上就地构造(memhub.NewMemPool):Check_Pool 要跨帧使用,而本函数的栈帧
# 在返回时即失效(与 VJson.JsonInit 同一理由,见 Debugger/memhub-lifetime.md)。
Pool: rawptr[memhub.MemPool] = memhub.NewMemPool(Check_Arena, CK_REPLY_POOL_CAP)
if Pool is None:
VLog.LogStr(" 警告: check 回执工作池构造失败,check 通道不可用\n")
return 0
Check_Pool = Pool
Check_Ready = 1
return 1
# ============================================================
# 请求行解析(看门狗分类与父进程执行共用同一口径)
# ============================================================
# 用仓库 json 库解析一行文本,判定是否为 check 请求。
# 返回 1 = 是(Req_Id 已填;Req_PathOk=1 时 Check_Path 也已填为 ANSI 路径),0 = 不是。
# 判定口径(需求:不得用字符串猜测):
# 顶层是 JSON 对象,且
# * 存在键 "check",其值是**非空字符串**(exe 路径);
# * 存在键 "id",其值是**整数**(json 库解析为 JSON_INT / int64)。
# 其余一切情况(顶层非对象、缺键、类型不符、空路径、非 JSON、空行)⇒ 0。
# 注意 "id" 的类型必须是**数字**(`"id": "1"` 是字符串,不算请求)。
# Req_PathOk=0 的语义是"请求成立、但路径缓冲不可用"(_mbuddy 未注入/池不可用):仍返回 1,
# 让调用方回执 CHANNEL_UNAVAILABLE、让看门狗照常转发(绝不让一条 check 请求被误当成心跳输入)。
# 实现:单遍遍历顶层子节点(与 VCache.FieldStr 同一只读遍历写法),两个键各自记"是否命中",
# 最后要求两者都命中——不依赖键的先后顺序,也不做任何字符串搜索。
def ParseCheckLine(Txt: str) -> int:
global Req_Id, Req_PathOk
Req_Id = 0
Req_PathOk = 0
if Txt is None:
return 0
L: t.CSizeT = string.strlen(Txt)
if L == 0 or L > t.CSizeT(CK_LINE_MAX):
return 0
Cap: t.CSizeT = L * t.CSizeT(CK_ARENA_PER_BYTE) + t.CSizeT(CK_ARENA_SLACK)
Arena: bytes = stdlib.malloc(Cap)
if Arena is None:
return 0
Pool: rawptr[memhub.MemPool] = memhub.MemPool(Arena, Cap)
if Pool is None:
stdlib.free(t.CPtr(Arena))
return 0
# 池容量预检(fail-closed):解析树的分配必须先确认放得下,否则 json.parse 内部的
# pool.alloc 会走到 includes/memhub.vp 的 printf + exit(1)。Need 与 Cap 同源且
# 新池 offset=0 ⇒ AllocOk(Need) 恒成立;此处只是把"容量推导被改小"变成可控降级。
if Pool.AllocOk(Cap) == 0:
stdlib.free(t.CPtr(Arena))
return 0
Root: rawptr[json.JsonValue] = json.parse(Pool, Txt)
HasCk: int = 0
HasId: int = 0
if Root != None and Root.vtype == json.JSON_OBJECT:
cu: rawptr[json.JsonValue] = Root.child
while cu != None:
if cu.key != None:
if string.strcmp(cu.key, "check") == 0:
if cu.vtype == json.JSON_STRING and cu.str_val != None:
if string.strlen(cu.str_val) > 0:
HasCk = 1
# 命令行/控制台输入按 UTF-8 送达;A 系列 API 需要 ANSI。
# Utf8ToAnsi 在"非法 UTF-8"时原样拷贝,故 ANSI 输入也照常可用。
# 路径缓冲不可用时保持 Req_PathOk=0,但仍算"是 check 请求"。
if EnsurePathBuf() != 0:
VFs.Utf8ToAnsi(cu.str_val, Check_Path, VFs.PATH_BUF_CAP)
Req_PathOk = 1
elif string.strcmp(cu.key, "id") == 0:
if cu.vtype == json.JSON_INT:
Req_Id = LONGLONG(cu.int_val)
HasId = 1
cu = cu.next
stdlib.free(t.CPtr(Arena))
if HasCk != 0 and HasId != 0:
return 1
return 0
# ParseCheckLine 的解析结果经模块全局出参暴露:Check_Path(ANSI 路径)、Req_Id(请求 id)
# 与 Req_PathOk(路径是否可用),与 VPe.Pe_RawPtr / VFs.LastReadSize 同一惯例
# (同模块内直接读,跨模块只走函数)。
# ============================================================
# 失败原因映射
# ============================================================
# PE 解析失败码(VPe.TryReadPeInfo 的 rc 表 + ParsePeHead 的 prc)→ 固定原因串。
# 映射依据(rc 表见 VPe.vp TryReadPeInfo / ParsePeHead 的函数头注释):
# 1 CreateFileA 失败 → 按 OpenErr:2/3=NOT_FOUND;5/32=NO_PERMISSION;其它=NO_PERMISSION
# 2 GetFileSizeEx 失败(读不出头部)→ NOT_EXE
# 3 文件过短 (<0x40) → NOT_EXE
# 4 头部窗口缓冲分配失败(读不出头部)→ NOT_EXE
# 5 ReadFull 读取不足 (<0x40) → NOT_EXE
# 6 raw_size==0(段内容为空)或段越界(段定位失败)→ PRVERSN_ERR
# 7 整文件读取失败(段读取失败) → PRVERSN_ERR
# 101 Got<0x40 / 102 非 MZ / 103 e_lfanew 越界 / 104 非 PE 签名 / 105 magic 未知 → NOT_EXE
# 106 节表超出头部窗口(无法定位节表 ⇒ 段定位失败)→ PRVERSN_ERR
# 107 未找到 .PRVERSN → NO_PRVERSN
def MapPeFailure(Rc: int, OpenErr: ULONG) -> str:
if Rc == 1:
if OpenErr == 2 or OpenErr == 3:
return CK_NOT_FOUND
return CK_NO_PERMISSION
if Rc == 5 or Rc == 107:
return CK_NO_PRVERSN
if Rc == 6 or Rc == 7:
return CK_PRVERSN_ERR
if Rc >= 100:
prc: int = Rc - 100
if prc == 7:
return CK_NO_PRVERSN
if prc == 6:
return CK_PRVERSN_ERR
return CK_NOT_EXE
return CK_NOT_EXE
# 把"拉起失败"的错误码格式化为 `EXE_ERR,0x<大写十六进制>`(写 Check_Msg,返回该串)。
# 例:GetLastError=2 → "EXE_ERR,0x2";193 → "EXE_ERR,0xC1";0 → "EXE_ERR,0x0"
# (0 只会出现在"未到达任何 Win32 调用"的失败阶段,如参数/路径非法)。
# 返回值指向模块级缓冲:调用方须立即使用(EmitReplyFail 会把它复制进回执池)。
def FormatExeErr(Err: ULONG) -> str:
viperlib.snprintf(Check_Msg, t.CSizeT(CK_MSG_CAP), "EXE_ERR,0x%X", Err)
return str(Check_Msg)
# ============================================================
# 检查执行
# ============================================================
# 对一个 exe 做完整检查(PE/.PRVERSN + --version)。
# 成功:返回 None,并把版本串/arch 写入 Check_Ver / Check_Arch、把 "Viper "+版本串
# 写入 Check_Disp(供调用方构造回执与入库)。
# 失败:返回固定的原因串(CK_* 常量或 FormatExeErr 的共享缓冲)。
# 说明:本函数**不做缓存写回**,只做"检查 + 填缓冲";入库与"已存在则不重写"由调用方决定。
def CheckExecutable(PathAnsi: str) -> str:
Check_Ver[0] = 0
Check_Arch[0] = 0
# ① PE / .PRVERSN(版本号与 arch 的静态来源)
rc: int = VPe.TryReadPeInfo(PathAnsi, Check_Ver, VPe.VER_CAP, Check_Arch, VPe.ARCH_CAP)
if rc != 0:
return MapPeFailure(rc, VPe.PeOpenErr())
if string.strlen(str(Check_Ver)) == 0:
return CK_PRVERSN_ERR # 段在但内容为空(第一个字节即 NUL)
# ② --version 验证(进程正常结束且前 5 字符为 Viper 才算通过)
pc: int = VProc.RunVersionProbe(PathAnsi, Check_Probe,
t.CSizeT(VProc.VERSION_PROBE_DIAG_CAP), 0)
if pc == 2 or VProc.Probe_TimedOut != 0:
return CK_TIMEOUT
if pc != 0:
# 拉起失败(参数/命令缓冲/管道/CreateProcessA):统一报 EXE_ERR,0x<码>
return FormatExeErr(VProc.ProbeLaunchErr())
if VProc.OutputHasViperPrefix(str(Check_Probe)) == 0:
# 拉起成功但输出前 5 字符不是 Viper ⇒ 独立原因 PREFIX_ERR(与段定位/读取/内容异常区分)
return CK_PREFIX_ERR
viperlib.snprintf(Check_Disp, t.CSizeT(VCache.DISP_CAP), "Viper %s", str(Check_Ver))
return None
# 完整重校一个已发现项:文件存在 + PE/.PRVERSN 可解析 + `--version` 前 5 字符为 Viper
# (即 CheckExecutable 的全部条件,只是不需要版本/arch 的结果)。
# 返回 1 = 通过(保留),0 = 不通过(调用方从缓存剔除)。
# fail-safe:CheckInit 失败(缓冲/池不可用)时返回 1(不剔除),避免把整表误清空。
# 该降级不逐条刷屏:CheckInit 只告警一次(按失败的那一组缓冲),扫描启动时由 VScan.ScanInit
# 再报一行"check 通道缓冲不可用"的轮级告警,两者共同说明"本轮不重校"的原因。
def VerifyExecutable(PathAnsi: str) -> int:
if CheckInit() == 0:
return 1
if CheckExecutable(PathAnsi) == None:
return 1
return 0
# ============================================================
# 入库(check 命中)
# ============================================================
# 把 check 命中项写入缓存(**已存在则不重写**:不改其 id/字段,直接跳过)。
# 返回 1 = 命中项已在缓存中(无论本轮是否新写入);0 = 缓存不可用。
# 幂等性:CacheFind 按 ANSI 路径精确匹配(与扫盘 HandleCandidate 同一判定),
# 因此同一 exe 反复 check 不会重复入表,也不会刷新既有 id/version/arch/source。
def TrackChecked(PathAnsi: str) -> int:
if VCache.CacheFind(PathAnsi) >= 0:
return 1
if VCache.CacheAdd(PathAnsi, str(Check_Id), str(Check_Ver), str(Check_Arch),
CK_SOURCE, str(Check_Disp)) == 0:
VLog.LogStr(" 警告: check 命中但入库失败(容量已满或分配失败),本轮不跟踪该条目\n")
return 0
return 1
# ============================================================
# 回执(stdout 单行 JSON)
# ============================================================
# 写一行 stdout(自带结尾 '\n',且**一次 WriteFile 写完**):
# json.write 的输出缓冲容量是 4096(缺省 cap),本文本远小于它,故 NUL 之后仍有空位放
# 换行与新的 NUL。这样做是为了让"一行回执"始终是**单次写**——与别人(看门狗失败行、
# 下一轮数组)写入不会在同一行中间交错。
def WriteReplyLine(Txt: str) -> int:
n: t.CSizeT = string.strlen(Txt)
if n + 2 <= t.CSizeT(4096):
Txt[n] = '\n'
Txt[n + 1] = 0
VLog.StdoutWrite(Txt)
return 1
# 不可达(长度守卫保证文本远小于 4096):仅为不写出无换行的残行而保留的后备分支
VLog.StdoutWrite(Txt)
VLog.StdoutWrite("\n")
return 1
# 成功回执:{"check_result": true, "id": <id>, "viper": {<发现项字典>}}
# viper 字典与既有发现项**同构同序**:id/executable/version/arch/source/displayName;
# source 固定 "check"(本次答复的来源)。已入库的 exe 用其缓存字段(id/version/arch/
# displayName 不重写、不改动),保证同一 exe 的 id 稳定。
def EmitReplyOk(ReqIdVal: LONGLONG, PathAnsi: str):
Id: str = str(Check_Id)
Ver: str = str(Check_Ver)
Arch: str = str(Check_Arch)
Disp: str = str(Check_Disp)
idx: int = VCache.CacheFind(PathAnsi)
if idx >= 0:
# 已在缓存中:不改其 id/字段("已存在则不写"),回执用缓存里的记录(id 稳定)
Id = VCache.CacheId(idx)
Ver = VCache.CacheVer(idx)
Arch = VCache.CacheArch(idx)
Disp = VCache.CacheDisp(idx)
else:
# 新命中:生成 UUID 后入库(失败仅告警:回执的 id 为空串,仍如实答复检查结果)
if VGuid.NewGuidStr(Check_Id, VGuid.GUID_STR_CAP) == 0:
VLog.LogStr(" 警告: CoCreateGuid 失败,check 入库项的 id 为空串\n")
Check_Id[0] = 0
Id = str(Check_Id)
TrackChecked(PathAnsi)
Check_Pool.reset()
ValNeed: t.CSizeT = t.CSizeT(CK_CONV_MAX) + t.CSizeT(CK_CONV_MAX)
ValNeed = ValNeed + string.strlen(Id) + string.strlen(Ver) + string.strlen(Arch)
ValNeed = ValNeed + string.strlen(CK_SOURCE)
if Check_Pool.AllocOk(t.CSizeT(CK_REPLY_NEED_FIXED) + ValNeed) == 0:
VLog.LogStr(" 警告: check 回执工作池不足,未回复(不会写穿缓冲)\n")
return
Obj: rawptr[json.JsonValue] = json.object(Check_Pool)
if Obj == None:
VLog.LogStr(" 警告: check 回执对象分配失败,未回复\n")
return
json.object_set(Check_Pool, Obj, "check_result", json.bool_val(Check_Pool, True))
json.object_set(Check_Pool, Obj, "id", json.int_val(Check_Pool, t.CInt64T(ReqIdVal)))
Sub: rawptr[json.JsonValue] = json.object(Check_Pool)
if Sub == None:
VLog.LogStr(" 警告: check 回执对象分配失败,未回复\n")
return
json.object_set(Check_Pool, Sub, "id", json.string_val(Check_Pool, Id))
# executable / displayName 转 UTF-8 后**立即**交给库复制(Check_Conv 随后要复用)
if VFs.AnsiToUtf8(PathAnsi, Check_Conv, VFs.CONV_CAP) == 0:
VFs.CopyRaw(Check_Conv, VFs.CONV_CAP, PathAnsi)
json.object_set(Check_Pool, Sub, "executable", json.string_val(Check_Pool, str(Check_Conv)))
json.object_set(Check_Pool, Sub, "version", json.string_val(Check_Pool, Ver))
json.object_set(Check_Pool, Sub, "arch", json.string_val(Check_Pool, Arch))
json.object_set(Check_Pool, Sub, "source", json.string_val(Check_Pool, CK_SOURCE))
if VFs.AnsiToUtf8(Disp, Check_Conv, VFs.CONV_CAP) == 0:
VFs.CopyRaw(Check_Conv, VFs.CONV_CAP, Disp)
json.object_set(Check_Pool, Sub, "displayName", json.string_val(Check_Pool, str(Check_Conv)))
json.object_set(Check_Pool, Obj, "viper", Sub)
Txt: str = json.write(Check_Pool, Obj, False)
if Txt == None:
VLog.LogStr(" 警告: check 回执序列化失败(工作池不足),未回复\n")
return
WriteReplyLine(Txt)
# 失败回执:{"check_result": false, "id": <id>, "check_msg": "<原因>"}
def EmitReplyFail(ReqIdVal: LONGLONG, Msg: str):
Check_Pool.reset()
Need: t.CSizeT = t.CSizeT(CK_REPLY_NEED_FIXED)
if Msg != None:
Need = Need + string.strlen(Msg)
if Check_Pool.AllocOk(Need) == 0:
VLog.LogStr(" 警告: check 回执工作池不足,未回复(不会写穿缓冲)\n")
return
Obj: rawptr[json.JsonValue] = json.object(Check_Pool)
if Obj == None:
VLog.LogStr(" 警告: check 回执对象分配失败,未回复\n")
return
json.object_set(Check_Pool, Obj, "check_result", json.bool_val(Check_Pool, False))
json.object_set(Check_Pool, Obj, "id", json.int_val(Check_Pool, t.CInt64T(ReqIdVal)))
json.object_set(Check_Pool, Obj, "check_msg", json.string_val(Check_Pool, Msg))
Txt: str = json.write(Check_Pool, Obj, False)
if Txt == None:
VLog.LogStr(" 警告: check 回执序列化失败(工作池不足),未回复\n")
return
WriteReplyLine(Txt)
# 降级回执(实际输出为紧凑单行):{"check_result": false, "id": <id>, "check_msg": "CHANNEL_UNAVAILABLE"}
# 仅在 CheckInit 失败(缓冲/回执池不可用)时使用,因此**不能**依赖 _mbuddy / Check_Pool:
# * 行缓冲走 stdlib.malloc(与 mb 池无关),写完立即 free(只在降级时发生,非常规路径);
# * 内容用固定模板 + viperlib.snprintf 直接产出,键名与固定 ASCII 原因串都无需转义,
# 与 json 库的输出形态一致(键序 check_result/id/check_msg、冒号后无空格、紧凑单行);
# 这样"请求已收到但通道不可用"对请求方始终可见,而不是静默不回复。
# 返回 1=已回执,0=连临时缓冲都拿不到(此时确实无法回复,只能不回复)。
def EmitReplyUnavailable(ReqIdVal: LONGLONG) -> int:
Fb: bytes = stdlib.malloc(CK_FALLBACK_CAP)
if Fb is None:
return 0
viperlib.snprintf(Fb, t.CSizeT(CK_FALLBACK_CAP),
"{\"check_result\":false,\"id\":%lld,\"check_msg\":\"%s\"}\n",
t.CInt64T(ReqIdVal), CK_CHANNEL_UNAVAILABLE)
VLog.StdoutWrite(str(Fb))
stdlib.free(t.CPtr(Fb))
return 1
# ============================================================
# 一行请求的完整处理(由 VGuard.PollIpc 在非阻塞轮询点调用)
# ============================================================
# 执行一条 check 请求行:解析 → 检查 → 回执。返回 1=已处理(无论成功/失败都已回执),
# 0=不是 check 请求/未处理。
# 防重入:Check_Handling 为 1 时直接返回 0——扫描侧的轮询点是"高频调用",但 check 的处理
# 不会从 check 内部再次发起轮询;该标记只是把"未来若在检查路径上新增轮询"这类改动挡住。
# 通道降级:CheckInit 失败(缓冲/回执池不可用)时不再静默丢弃,而是先识别请求再回执
# CHANNEL_UNAVAILABLE(EmitReplyUnavailable),使"请求已收到、但无法执行检查"可被请求方区分。
def HandleCheckLine(Txt: str) -> int:
global Check_Handling
if Check_Handling != 0:
return 0
if CheckInit() == 0:
if ParseCheckLine(Txt) == 0:
return 0
EmitReplyUnavailable(Req_Id)
return 1
if ParseCheckLine(Txt) == 0:
return 0
Check_Handling = 1
Path: str = str(Check_Path)
Reason: str = CheckExecutable(Path)
if Reason == None:
EmitReplyOk(Req_Id, Path)
else:
EmitReplyFail(Req_Id, Reason)
Check_Handling = 0
return 1
+538
View File
@@ -0,0 +1,538 @@
import t, c
from stdint import *
import string
import sys
import lib.VLog as VLog
import lib.VDirs as VDirs
# ============================================================
# VCli - 命令行解析(scan/ver、--cache/--once/--sleep/--pace/--pace-units/--need-confirm/
# --need-confirm-timeout/--path/--verbose/--search-paths;手写解析,不引入 argparse)
#
# 从原 App/main.vp 的 RunScan 参数解析段平移,并新增可重复的 --path(需求 2):
# 一旦出现 --path 即置 VDirs 覆盖模式(仅收集路径、保序);存在性检查与
# "不存在则告警并跳过该条"在 VDirs.BuildTable([0/3] 段内)执行。
# --need-confirm <秒>:心跳确认间隔,缺省/0 = 关闭该功能(仅 scan 模式生效)。
# 本轮改为"独立看门狗子进程"实现(见 VGuard.vp):父进程在本模块解析出
# NeedConfirm/NeedConfirmTimeout 后由 App/main.vp 拉起子进程,扫盘侧不再参与确认。
# --sleep <毫秒>:手动指定扫盘轮间隔(缺省 DEFAULT_SLEEP_MS=10000;0 = 不 sleep)。
# --pace <毫秒>:扫盘节流点的睡眠时长(缺省 DEFAULT_PACE_SLEEP_MS=20;0 = 关闭节流)。
# --pace-units <N>:文件侧节流粒度——每 N 个文件一个节流点(缺省 DEFAULT_PACE_FILE_UNITS=64;1 = 每个文件)。
# 两者"显式给出即启用":只要命令行出现其一,VScan 便打开节流(--once 下也生效);
# 未给出时 VScan 完全沿用其自身缺省(常驻启用/20ms/64,--once 关闭),行为逐字不变。
# 目录侧粒度固定为 VScan.PACE_DIR_UNITS=16,不受 --pace-units 影响;--pace 0 时两个节流点都不 Sleep。
# 取值经 PaceSet()/PaceSleepMs()/PaceFileUnits() 暴露给 VScan.RunScan。
# --need-confirm-timeout <毫秒>:看门狗确认等待超时(缺省 DEFAULT_CONFIRM_TIMEOUT_MS=5000)。
# --verbose(无值):打开详细过程日志(缺省 = 安静)。仅改变 stderr 日志详细度,
# 不影响 stdout 的单行 JSON 与缓存文件内容。取值经 Verbose() 暴露,由 App/main.vp
# 在参数解析后、任何日志之前设置到 VLog(VLog.SetVerbose)。
# --search-paths(无值):**查询开关**——建好"会搜索的目录表"后逐行打印(stderr)并直接退出,
# 不扫盘、不拉起看门狗、不读写缓存,因此与 --cache 相互独立:未给 --cache 也可运行
# (见 ParseScanArgs 末尾的必填校验)。取值经 SearchPaths() 暴露,由 App/main.vp 在 scan
# 分派中"拉起看门狗之前"分派到 VScan.RunSearchPathsQuery。
# confirm-watch(内部隐藏子命令)的参数解析在 ParseWatchArgs(),不出现在用法里;
# 父进程为 verbose 时为子进程追加 --verbose,使看门狗按同一规则输出(WatchVerbose())。
# ver 子命令的取参(sys._argv[2])在 VPe.RunVer 内,保持原样;ver 不解析开关,
# 其 verbose 由 HasVerbose()(argv 中出现 --verbose 即置位)识别。
# 取参方式依据:sys._init_argv/_argc/_argv(includes/sys.vp:60-67)。
# ============================================================
VET_VERSION: t.CDefine = "0.2.0"
SECS_MAX: t.CDefine = 86400 # --need-confirm 秒数上界(1 天;超出视为参数错误)
DEFAULT_SLEEP_MS: t.CDefine = 10000 # --sleep 缺省值(沿用原 SCAN_INTERVAL_MS)
SLEEP_MAX_MS: t.CDefine = 86400000 # --sleep 毫秒上界(1 天;超出视为参数错误)
DEFAULT_CONFIRM_TIMEOUT_MS: t.CDefine = 5000 # --need-confirm-timeout 缺省值
CONFIRM_TIMEOUT_MAX_MS: t.CDefine = 86400000 # --need-confirm-timeout 毫秒上界(1 天)
# --pace <毫秒>:节流点睡眠时长,取值域 0..60000。0 = 关闭节流(两个节流点都不 Sleep)。
# 上界取 1 分钟:节流只为降低扫盘对机器的持续压力,睡眠远超 1 分钟只会无谓拖慢单轮而无收益,
# 与 --sleep(轮间隔,上界 1 天)的用途不同,故不共用其宽松上界。
DEFAULT_PACE_SLEEP_MS: t.CDefine = 20 # --pace 缺省值(沿用 VScan.PACE_SLEEP_MS,未给出时逐字保持现状)
PACE_SLEEP_MAX_MS: t.CDefine = 60000 # --pace 毫秒上界(1 分钟)
# --pace-units <N>:文件粒度,取值域 1..1000000(每 N 个文件一个节流点;1 = 每个文件都停)。
# 0 无意义("每 0 个文件"),与非法输入一并报错;上界 1000000 已足够稀疏(等同关闭文件侧节流)。
DEFAULT_PACE_FILE_UNITS: t.CDefine = 64 # --pace-units 缺省值(沿用 VScan.PACE_FILE_UNITS,未给出时逐字保持现状)
PACE_UNITS_MAX: t.CDefine = 1000000 # --pace-units 文件数上界
# --parent-pid 上界(DWORD 全域 4294967295)。
# 注意:这里不能用 `t.CDefine = 4294967295`。CDefine 的物化规则(TPV
# ExprGenCdef._const_int_for_cdefine)把落在 u32 范围内的值一律按 **i32** 常量生成
# (0xFFFFFFFF → i32 -1),上一轮构建 IR 现场:
# VET/.tpv_build/temp/df/df86f78f0f0481a1.ll:1214
# %108 = call i32 @...ParseUint(i8* %107, i32 -1)
# 即 PID_MAX 传到 `Max: int` 时已成 -1。改宽类型全局常量后该值以 i64 物化
# (字面量对 >32 位目标走 const_int64,ExprGenConst.translate_constant:105-107)。
PID_MAX: LONGLONG = 4294967295 # --parent-pid 上界(DWORD 全域)
Cache_Path: str = None
Once_Flag: int = 0
Need_Confirm: int = 0
Sleep_Ms: int = DEFAULT_SLEEP_MS
Need_Confirm_Timeout: int = DEFAULT_CONFIRM_TIMEOUT_MS
Verbose_Flag: int = 0 # --verbose(scan);1=详细日志
# 顶层写法(`VET.exe --search-paths ...`,子命令位置直接写该开关)时由 App/main.vp 置位:
# ParseScanArgs 从 argv[2] 起扫并先把 Search_Paths_Flag 清零,故"子命令位置上的那个
# --search-paths"它扫不到;用本标记在解析后重新并入,两种写法语义完全一致。
Search_Paths_Implicit: int = 0
Search_Paths_Flag: int = 0 # --search-paths(scan);1=打印将搜索的路径后直接退出
# --pace/--pace-units(scan):显式给出即启用节流(--once 下也生效)。
# Pace_Set 记"是否显式给出过两者之一";未给出时 VScan 完全沿用其缺省(常驻启用/20ms/64,--once 关闭)。
Pace_Set: int = 0 # 1=命令行显式给出 --pace 或 --pace-units
Pace_Ms: int = DEFAULT_PACE_SLEEP_MS # --pace <毫秒>(0 = 关闭节流)
Pace_Units: int = DEFAULT_PACE_FILE_UNITS # --pace-units <N>(每 N 个文件一个节流点;1 = 每个文件)
# --first-no-pace(scan):第一轮扫盘不启用节流(之后各轮按常规规则)。
# 只影响"第 1 轮"这一件事:不改变 Pace_Set/Pace_Ms/Pace_Units,也不改变常驻/--once 的常规节流规则;
# 与 --pace 0(所有轮都不节流)互不替代——前者是"首轮抢速度",后者是"全程不限速"。
First_No_Pace_Flag: int = 0 # 1=第一轮不启用节流
# confirm-watch(隐藏内部子命令)参数
Watch_Interval: int = 0 # 秒(父进程由 --need-confirm 传入)
Watch_Timeout: int = -1 # 毫秒;-1 = 未给出(之后取 DEFAULT_CONFIRM_TIMEOUT_MS)
Watch_ParentPid: LONGLONG = 0 # 父进程 pid(宽类型:承载 DWORD 全域,见 PID_MAX)
Watch_IpcWrite: ULONGLONG = 0 # check 通道管道**写端句柄数值**(0 = 未给出 ⇒ 无通道)
Watch_Verbose: int = 0 # --verbose(由父进程在 verbose 时追加传入)
# ParseUint64 的出参(模块全局,与 VPe.Pe_RawPtr / VFs.LastReadSize 同一惯例)
U64_Val: ULONGLONG = 0
# 版本号经函数暴露给 VPe/VScan(跨模块"函数调用"路径明确支持;
# 避免依赖跨模块字符串 t.CDefine 取值——本仓已证实的是跨模块整型 t.CDefine)
def Version() -> str:
return VET_VERSION
def CachePath() -> str:
return Cache_Path
def Once() -> int:
return Once_Flag
def NeedConfirm() -> int:
return Need_Confirm
def SleepMs() -> int:
return Sleep_Ms
def NeedConfirmTimeout() -> int:
return Need_Confirm_Timeout
def Verbose() -> int:
return Verbose_Flag
def SearchPaths() -> int:
return Search_Paths_Flag
# --pace/--pace-units 取值:PaceSet()==0 时调用方(VScan.RunScan)应完全忽略后两者、沿用自身缺省,
# 以保证"未给出参数时行为逐字不变";==1 时三者的组合语义见 RunScan 的注释。
def PaceSet() -> int:
return Pace_Set
def PaceSleepMs() -> int:
return Pace_Ms
def PaceFileUnits() -> int:
return Pace_Units
# --first-no-pace:是否要求"第 1 轮不启用节流"。1=是(仅第 1 轮;之后各轮按常规规则)。
def FirstNoPace() -> int:
return First_No_Pace_Flag
# 顶层写法专用:`VET.exe --search-paths ...`(子命令位置就是该开关)。
# 必须在 ParseScanArgs 之前调用——它把该意图记进 Search_Paths_Implicit,解析结束时再并入
# Search_Paths_Flag;这样解析里"Search_Paths_Flag == 0 才要求 --cache"的放宽也一并生效。
def SetSearchPathsImplicit():
global Search_Paths_Implicit
Search_Paths_Implicit = 1
def WatchInterval() -> int:
return Watch_Interval
def WatchTimeout() -> int:
return Watch_Timeout
def WatchParentPid() -> LONGLONG:
return Watch_ParentPid
def WatchIpcWrite() -> ULONGLONG:
return Watch_IpcWrite
def WatchVerbose() -> int:
return Watch_Verbose
# ver 子命令专用:ver 不解析开关(目标路径固定在 sys._argv[2]),
# 只要 argv 中出现 --verbose 即视为要求详细日志(argv 起点与 scan 一致,从下标 2 扫)。
def HasVerbose() -> int:
i: int = 2
while i < sys._argc:
A: str = sys._argv[i]
if A is not None:
if string.strcmp(A, "--verbose") == 0:
return 1
i += 1
return 0
# 严格解析十进制非负整数:仅接受 '0'-'9'(空串/空白/正负号/小数点/其它字符 → -1=非法),
# 上界 Max(超出 → -1,避免无意义超大间隔与运算溢出)。
# 不用 string.atoi(includes/string.vp:271):它对非法尾字符静默截断("3x" → 3),
# 与本参数"整数字面量"的约定不符。
#
# 宽度(本函数修的就是这一条):Max / 累加器 v / 返回值统一用 LONGLONG(有符号 64 位)。
# * 原 `Max: int`(i32)配 PID_MAX=4294967295 会被截断成 -1,于是 `v > Max`
# 对任何首位数字恒真 ⇒ 任何 --parent-pid 都判非法(看门狗启动即 exit 1)。
# IR 证据:本仓上一轮构建 IR
# VET/.tpv_build/temp/df/df86f78f0f0481a1.ll:521
# define i32 @df86f78f0f0481a1.ParseUint(i8* %S, i32 %Max) ; Max 是 i32
# VET/.tpv_build/temp/df/df86f78f0f0481a1.ll:583
# %35 = icmp sgt i32 %34, %Max ; 有符号比较,Max=-1
# VET/.tpv_build/temp/df/df86f78f0f0481a1.ll:1214
# %108 = call i32 @df86f78f0f0481a1.ParseUint(i8* %107, i32 -1)
# 后跟 :1217 `%110 = icmp sle i32 %109, 0` ⇒ P<=0 恒真 ⇒ 打错误并 ret 1。
# * 不用 t.CSizeT/ULONGLONG:无符号返回承载不了 -1 非法哨兵,且各调用点的
# `if X < 0`(本文件 :165/:180/:196/:253/:264/:275)在无符号比较下恒假 ⇒ 非法值被放行。
# * 溢出(20 位数字也不会):循环顶部 `if v > Max: return -1` 是不变量守卫——
# 进入每轮时必有 v <= Max,故 v*10 <= Max*10;本模块全部上界 <= 4294967295,
# 远小于 i64 上限/10,`v*10 + d` 不可能溢出;末尾 `or v < 0` 再兜一层 64 位
# 回绕(防御性,按当前上界不可达)。
def ParseUint(S: str, Max: LONGLONG) -> LONGLONG:
if S is None:
return -1
ub: BYTEPTR = BYTEPTR(S)
if ub[0] == 0:
return -1
v: LONGLONG = 0
i: int = 0
while ub[i] != 0:
ch: int = int(ub[i])
if ch < 48 or ch > 57:
return -1
if v > Max: # 上界短路:兼作 v*10 的溢出守卫(不变量兜底)
return -1
v = v * 10 + LONGLONG(ch - 48)
if v > Max or v < 0: # 越界 / 64 位回绕(后者理论不可达)
return -1
i += 1
return v
# --need-confirm 的秒数解析:等价 ParseUint(S, SECS_MAX)(保持原判定口径与错误码 -1)
def ParseSecs(S: str) -> int:
return ParseUint(S, SECS_MAX)
# 严格解析十进制**无符号 64 位**字面量:仅接受 '0'-'9'(空串/空白/正负号/小数点/其它字符/溢出 ⇒ 失败)。
# 成功返回 1 并把值写入 U64_Val;失败返回 0(U64_Val 置 0)。
#
# 为什么不复用 ParseUint:`--ipc-write` 传的是**句柄的位模式**(父进程 ptrtoint 得到)。
# 64 位 Windows 上内核句柄按符号扩展存放(最高位为 1),其无符号读数超过 INT64_MAX,
# 而 ParseUint 的返回域(LONGLONG)与 -1 非法哨兵都承载不了它;若截成 32 位传,符号扩展型
# 句柄在子进程侧就还原不回原值(inttoptr 得到另一个地址)。故这里按 u64 全域解析。
# 溢出守卫:不变量 v*10 + d ≤ 2^64-1 = 18446744073709551615 ⇒ v ≤ 1844674407370955161,
# 且 v 取该上界时 d 必须 ≤ 5。
def ParseUint64(S: str) -> int:
global U64_Val
U64_Val = 0
if S is None:
return 0
ub: BYTEPTR = BYTEPTR(S)
if ub[0] == 0:
return 0
v: ULONGLONG = 0
i: int = 0
while ub[i] != 0:
ch: int = int(ub[i])
if ch < 48 or ch > 57:
return 0
d: ULONGLONG = ULONGLONG(ch - 48)
if v > ULONGLONG(1844674407370955161): # 再乘 10 必溢出
return 0
if v == ULONGLONG(1844674407370955161) and d > ULONGLONG(5):
return 0
v = v * ULONGLONG(10) + d
i += 1
U64_Val = v
return 1
def PrintUsage():
VLog.LogStr("VET - Viper Environment Tool\n")
VLog.LogStr("用法:\n")
VLog.LogStr(" VET.exe ver <exe> [--verbose]\n")
VLog.LogStr(" 单次校验指定 exe(常用目录表 + .PRVERSN 段 + --version 前缀);\n")
VLog.LogStr(" --verbose 为与 scan 同名的无值开关(输出目标路径与解析/探测细节)。\n")
VLog.LogStr(" VET.exe scan --cache <path> [--once] [--sleep <毫秒>] [--pace <毫秒>] [--pace-units <N>]\n")
VLog.LogStr(" [--first-no-pace] [--need-confirm <秒> [--need-confirm-timeout <毫秒>]]\n")
VLog.LogStr(" [--verbose] [--path <dir>]...\n")
VLog.LogStr(" 常驻扫盘发现器;结果 JSON 写 stdout,缓存写 <path>。\n")
VLog.LogStr(" VET.exe scan --search-paths [--path <dir>]... [--verbose]\n")
VLog.LogStr(" VET.exe --search-paths [--path <dir>]... [--verbose] (同上,可省 scan:纯查询)\n")
VLog.LogStr(" --once 只跑一轮(便于验证)。\n")
VLog.LogStr(" --sleep <毫秒> 每轮间隔(缺省 10000;0 = 不 sleep;上限 86400000)。\n")
VLog.LogStr(" --pace <毫秒> 节流点睡眠时长(缺省 20;0 = 关闭节流;上限 60000)。\n")
VLog.LogStr(" --pace-units <N> 每 N 个文件一个节流点(缺省 64;1 = 每个文件都停;上限 1000000)。\n")
VLog.LogStr(" 显式给出 --pace 或 --pace-units 即启用节流,--once 下也生效;\n")
VLog.LogStr(" 未给出时沿用缺省(常驻启用;--once 关闭)。目录侧粒度恒为 16。\n")
VLog.LogStr(" --first-no-pace 第 1 轮扫盘不启用节流(只影响第 1 轮;之后各轮按常规规则)。\n")
VLog.LogStr(" 与 --pace 0 的区别:后者是全程不限速。仅常驻多轮时才有意义。\n")
VLog.LogStr(" --need-confirm <秒> 心跳确认:独立看门狗子进程每 <秒> 秒要求 stdin 输入一行\n")
VLog.LogStr(" {\"ok\": 1};超时内未输入或 ok!=1 则输出 {\"exit\":1, ...}\n")
VLog.LogStr(" 并终止本进程(退出码 1)。缺省/0 = 关闭;仅 scan 模式生效。\n")
VLog.LogStr(" --need-confirm-timeout <毫秒>\n")
VLog.LogStr(" 确认等待超时(缺省 5000;仅在给出 --need-confirm 时有意义)。\n")
VLog.LogStr(" --verbose 输出详细过程日志(进度/扫描根/逐条过滤/探测细节);\n")
VLog.LogStr(" 缺省安静:只留启动摘要、心跳确认提示、警告与轮末一行。\n")
VLog.LogStr(" --path <dir> 强制替换常用目录表(可重复);一出现即不做任何自动建表。\n")
VLog.LogStr(" --search-paths 打印将搜索的路径后退出:建好常用目录表即逐行列出\n")
VLog.LogStr(" (序号 + 路径 + 目录表来源,输出到 stderr),不扫盘、\n")
VLog.LogStr(" 不拉起看门狗、不读写缓存;此时 --cache 非必填。\n")
# 解析 scan 参数。返回 0=成功,1=参数错误(已打印用法/错误)
# 说明:本轮起 ParseScanArgs 由 App/main.vp 在分派 scan 时调用(看门狗生命周期需要
# 在 RunScan 之前拿到 NeedConfirm),VScan.RunScan 不再自行解析。
def ParseScanArgs() -> int:
global Cache_Path, Once_Flag, Need_Confirm, Sleep_Ms, Need_Confirm_Timeout, Verbose_Flag
global Search_Paths_Flag, Pace_Set, Pace_Ms, Pace_Units, First_No_Pace_Flag
Cache_Path = None
Once_Flag = 0
Need_Confirm = 0
Sleep_Ms = DEFAULT_SLEEP_MS
Need_Confirm_Timeout = DEFAULT_CONFIRM_TIMEOUT_MS
Verbose_Flag = 0
Search_Paths_Flag = 0
Pace_Set = 0
Pace_Ms = DEFAULT_PACE_SLEEP_MS
Pace_Units = DEFAULT_PACE_FILE_UNITS
First_No_Pace_Flag = 0
if Search_Paths_Implicit != 0:
# 顶层写法(`VET.exe --search-paths ...`):该开关位于 argv[1](子命令位置),
# 本函数从 argv[2] 起扫,扫不到它 ⇒ 由 main.vp 预先置位,这里并入(见其声明处)。
Search_Paths_Flag = 1
Has_Timeout: int = 0
i: int = 2
while i < sys._argc:
A: str = sys._argv[i]
if A is not None:
if string.strcmp(A, "--cache") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
Cache_Path = sys._argv[i + 1]
i += 1
else:
VLog.LogStr("错误: --cache 需要一个路径参数\n")
return 1
elif string.strcmp(A, "--once") == 0:
Once_Flag = 1
elif string.strcmp(A, "--verbose") == 0:
Verbose_Flag = 1 # 无值开关:只改 stderr 详细度
elif string.strcmp(A, "--search-paths") == 0:
Search_Paths_Flag = 1 # 无值开关:查询"会搜索的路径"后直接退出(不扫盘/不碰缓存)
elif string.strcmp(A, "--sleep") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
M: int = ParseUint(sys._argv[i + 1], SLEEP_MAX_MS)
if M < 0:
VLog.LogFmt1("错误: --sleep 需要一个 0..%u 的整数字面量毫秒数,实际为: ",
t.CSizeT(SLEEP_MAX_MS))
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
PrintUsage()
return 1
Sleep_Ms = M
i += 1
else:
VLog.LogStr("错误: --sleep 需要一个毫秒数参数\n")
PrintUsage()
return 1
elif string.strcmp(A, "--pace") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
P: int = ParseUint(sys._argv[i + 1], PACE_SLEEP_MAX_MS)
if P < 0:
VLog.LogFmt1("错误: --pace 需要一个 0..%u 的整数字面量毫秒数,实际为: ",
t.CSizeT(PACE_SLEEP_MAX_MS))
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
PrintUsage()
return 1
Pace_Ms = P
Pace_Set = 1 # 显式给出 ⇒ 节流启用(--once 下也生效)
i += 1
else:
VLog.LogStr("错误: --pace 需要一个毫秒数参数\n")
PrintUsage()
return 1
elif string.strcmp(A, "--pace-units") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
U: int = ParseUint(sys._argv[i + 1], PACE_UNITS_MAX)
# 0 与非法同为错误(ParseUint 非法返回 -1):0 会让"每 0 个文件一个节流点"无意义
if U <= 0:
VLog.LogFmt1("错误: --pace-units 需要一个 1..%u 的整数字面量文件数,实际为: ",
t.CSizeT(PACE_UNITS_MAX))
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
PrintUsage()
return 1
Pace_Units = U
Pace_Set = 1 # 显式给出 ⇒ 节流启用(--once 下也生效)
i += 1
else:
VLog.LogStr("错误: --pace-units 需要一个文件数参数\n")
PrintUsage()
return 1
elif string.strcmp(A, "--first-no-pace") == 0:
# 无值开关:只要求"第 1 轮不节流",不改变节流参数本身、也不置 Pace_Set
# (第 2 轮起的节流规则与未给该开关时逐字相同)。
First_No_Pace_Flag = 1
elif string.strcmp(A, "--need-confirm") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
S: int = ParseSecs(sys._argv[i + 1])
if S < 0:
# 两个固定元数调用拼一行(LogFmt1 只带整数,LogFmtS1 只带字符串)
VLog.LogFmt1("错误: --need-confirm 需要一个 0..%u 的整数字面量秒数,实际为: ",
t.CSizeT(SECS_MAX))
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
PrintUsage()
return 1
Need_Confirm = S
i += 1
else:
VLog.LogStr("错误: --need-confirm 需要一个秒数参数\n")
PrintUsage()
return 1
elif string.strcmp(A, "--need-confirm-timeout") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
T: int = ParseUint(sys._argv[i + 1], CONFIRM_TIMEOUT_MAX_MS)
if T < 0:
VLog.LogFmt1("错误: --need-confirm-timeout 需要一个 0..%u 的整数字面量毫秒数,实际为: ",
t.CSizeT(CONFIRM_TIMEOUT_MAX_MS))
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
PrintUsage()
return 1
Need_Confirm_Timeout = T
Has_Timeout = 1
i += 1
else:
VLog.LogStr("错误: --need-confirm-timeout 需要一个毫秒数参数\n")
PrintUsage()
return 1
elif string.strcmp(A, "--path") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
VDirs.BeginOverride()
VDirs.AddOverridePath(sys._argv[i + 1])
i += 1
else:
VLog.LogStr("错误: --path 需要一个目录参数\n")
return 1
else:
VLog.LogFmtS1("错误: 未知参数 %s\n", A)
PrintUsage()
return 1
i += 1
# --search-paths 是纯查询开关(只建表 + 打印,不读写缓存、不扫盘):未给 --cache 时放行。
# 其余校验一律不放宽——给了 --cache 仍按原样接受(该查询只是不使用它)。
if Cache_Path is None and Search_Paths_Flag == 0:
VLog.LogStr("错误: scan 模式必须提供 --cache <path>\n")
PrintUsage()
return 1
# 取舍:--need-confirm-timeout 单独出现时不报错(仅告警忽略)——它只是给
# --need-confirm 的从属参数,报错会因标志顺序/复用脚本而无谓失败;告警避免静默无效。
if Has_Timeout != 0 and Need_Confirm <= 0:
VLog.LogStr("警告: --need-confirm-timeout 未与 --need-confirm 一起使用,已忽略\n")
return 0
# ============================================================
# confirm-watch(内部隐藏子命令)参数解析
# 命令行形如:VET.exe confirm-watch --interval <秒> --timeout <毫秒> --parent-pid <pid>
# --ipc-write <句柄数值> [--verbose]
# --interval 必填且 >=1;--timeout 缺省 DEFAULT_CONFIRM_TIMEOUT_MS;--parent-pid 必填。
# --ipc-write 由父进程在建立 check 通道管道时传入(写端句柄的数值);缺省 0 = 无通道。
# --verbose 由父进程在自身为 verbose 时追加,使看门狗按同一规则输出细节。
# 该命令不出现在用法文本里,由父进程(scan + --need-confirm)自动拉起。
# ============================================================
def ParseWatchArgs() -> int:
global Watch_Interval, Watch_Timeout, Watch_ParentPid, Watch_IpcWrite, Watch_Verbose
Watch_Interval = 0
Watch_Timeout = -1
Watch_ParentPid = 0
Watch_IpcWrite = 0
Watch_Verbose = 0
Has_Pid: int = 0
i: int = 2
while i < sys._argc:
A: str = sys._argv[i]
if A is not None:
if string.strcmp(A, "--interval") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
S: int = ParseUint(sys._argv[i + 1], SECS_MAX)
if S <= 0:
VLog.LogStr("confirm-watch: --interval 需要一个 >=1 的整数字面量秒数\n")
return 1
Watch_Interval = S
i += 1
else:
VLog.LogStr("confirm-watch: --interval 需要一个秒数参数\n")
return 1
elif string.strcmp(A, "--timeout") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
T: int = ParseUint(sys._argv[i + 1], CONFIRM_TIMEOUT_MAX_MS)
if T < 0:
VLog.LogStr("confirm-watch: --timeout 需要一个非负整数字面量毫秒数\n")
return 1
Watch_Timeout = T
i += 1
else:
VLog.LogStr("confirm-watch: --timeout 需要一个毫秒数参数\n")
return 1
elif string.strcmp(A, "--parent-pid") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
P: LONGLONG = ParseUint(sys._argv[i + 1], PID_MAX)
if P <= 0:
VLog.LogStr("confirm-watch: --parent-pid 需要一个正整数\n")
return 1
Watch_ParentPid = P
Has_Pid = 1
i += 1
else:
VLog.LogStr("confirm-watch: --parent-pid 需要一个 pid 参数\n")
return 1
elif string.strcmp(A, "--ipc-write") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
if ParseUint64(sys._argv[i + 1]) == 0:
VLog.LogFmtS1("confirm-watch: --ipc-write 需要一个十进制非负整数字面量句柄值,实际为: ",
sys._argv[i + 1])
VLog.LogStr("\n")
return 1
Watch_IpcWrite = U64_Val
i += 1
else:
VLog.LogStr("confirm-watch: --ipc-write 需要一个句柄数值参数\n")
return 1
elif string.strcmp(A, "--verbose") == 0:
Watch_Verbose = 1 # 无值开关:由父进程按自身 verbose 追加
else:
VLog.LogFmtS1("confirm-watch: 未知参数 %s\n", A)
return 1
i += 1
if Watch_Interval <= 0:
VLog.LogStr("confirm-watch: 缺少 --interval <秒>\n")
return 1
if Has_Pid == 0:
VLog.LogStr("confirm-watch: 缺少 --parent-pid <pid>\n")
return 1
if Watch_Timeout < 0:
Watch_Timeout = DEFAULT_CONFIRM_TIMEOUT_MS
return 0
+302
View File
@@ -0,0 +1,302 @@
import t, c
from stdint import *
import stdlib
import string
import memhub
import lib.VFs as VFs
import lib.VLog as VLog
import w32.win32base as wb
# ============================================================
# VDirs - 常用目录表(自动构建 / --path 强制覆盖)
#
# 从原 App/main.vp 的 [0/3] 段平移;本轮新增"覆盖表"语义(需求 2):
# * 未给 --path:完全保持原自动建表(静态两条 + 各盘 Program Files + 桌面);
# * 给过 --path:不做任何自动构建,表 = 命令行给出的路径序列(保序、去重)。
# 其它模块(VPe/VScan)只经 DirCount()/DirAt(i) 读取本表,不交叉写全局。
#
# 日志详细度:目录表逐条列举、来源统计与"桌面兜底所用路径"属过程细节,走 VLog.Verbose*;
# "警告:" 类告警(--path 数量超限/表满/--path 不存在/桌面不可用/盘符枚举失败)始终输出;
# 其中被发现/处理的目录路径不再内嵌(仅 --verbose 时给出),命令行 --path 参数自身的回显除外。
#
# 依赖本仓既有:wb.GetLastError(:62)、
# VFs.PathIsDir / VFs.WideToUtf8Needed / VFs.WideToUtf8(本包 VFs.vp,
# 内部用 wf.GetFileAttributesA(includes/w32/win32file.vp:99) 与
# wb.MultiByteToWideChar(:94))。
#
# Win32 自声明(本仓无声明,按 Win32 原型自声明;x64 统一调用约定):
# kernel32!GetLogicalDrives: DWORD GetLogicalDrives(void);
# kernel32!GetDriveTypeA: UINT GetDriveTypeA(LPCSTR);
# kernel32!GetEnvironmentVariableA: DWORD GetEnvironmentVariableA(LPCSTR,LPSTR,DWORD);
# kernel32!SetErrorMode: UINT SetErrorMode(UINT);
# shell32!SHGetKnownFolderPath: HRESULT SHGetKnownFolderPath(REFKNOWNFOLDERID,DWORD,HANDLE,PWSTR*);
# ole32!CoTaskMemFree: void CoTaskMemFree(LPVOID);
# 原型出处:Win32 API 文档(winbase.h / shlobj_core.h / combaseapi.h)。
# ============================================================
# Win32: typedef LONG HRESULT(有符号 32 位,负值才是失败)
HRESULT: t.CTypedef = t.CInt32T
# Win32: typedef const KNOWNFOLDERID *REFKNOWNFOLDERID(指向 16 字节 GUID)
REFKNOWNFOLDERID: t.CTypedef = t.CPtr
extern def GetLogicalDrives() -> DWORD
extern def GetDriveTypeA(lpRootPathName: wb.LPCSTR) -> UINT
extern def GetEnvironmentVariableA(lpName: wb.LPCSTR, lpBuffer: CHARPTR, nSize: DWORD) -> DWORD
extern def SetErrorMode(uMode: UINT) -> UINT
extern def SHGetKnownFolderPath(rfid: REFKNOWNFOLDERID, dwFlags: DWORD, hToken: wb.HANDLE,
ppszPath: t.CPtr) -> HRESULT
extern def CoTaskMemFree(pv: t.CPtr)
SEM_FAILCRITICALERRORS: t.CDefine = 0x0001 # SetErrorMode 标志(errhandlingapi.h)
KNOWNFOLDERID_SIZE: t.CDefine = 16 # KNOWNFOLDERID / GUID 字节数
# winnt.h: FILE_ATTRIBUTE_REPARSE_POINT 在 VScan 使用
# GetDriveTypeA 返回值(winbase.h)
DRIVE_NO_ROOT_DIR: t.CDefine = 1 # 跳过:无根路径
DRIVE_REMOVABLE: t.CDefine = 2 # 探测:可移动介质(U 盘/读卡器)
DRIVE_FIXED: t.CDefine = 3 # 探测:定长磁盘
DRIVE_REMOTE: t.CDefine = 4 # 探测:网络盘
DRIVE_CDROM: t.CDefine = 5 # 跳过:光驱
# 决策:只跳过 NO_ROOT_DIR(1) 与 CDROM(5),其余(含 REMOVABLE)继续探测 Program Files;
# 空介质/卡读卡器触发的弹窗与阻塞由 AddFixedDriveProgramFiles 里的 SetErrorMode 抑制。
COMMON_DIR_CAPACITY: t.CDefine = 128 # 表容量(内联数组元素个数)
DRIVE_ROOT_CAP: t.CDefine = 8 # "X:\" + NUL 缓冲
DESKTOP_SUFFIX_CAP: t.CDefine = 9 # r"\Desktop"(8) + NUL(1)
# 由 App/main.vp 注入
_mbuddy: rawptr[memhub.MemBuddy]
# 显式长度内联数组 + 计数:避免 t.CArray[str] 单参数形态在 TPC/TransPyV 两链
# 语义不一致(TPC=自动长度内联数组,TransPyV=指针)
Common_Directory: t.CArray[str, COMMON_DIR_CAPACITY]
Common_Dir_Count: int = 0 # 已用槽位数
# --path 覆盖项(保序;解析期只收集,构建期才做存在性检查并告警)
Override_Dir: t.CArray[str, COMMON_DIR_CAPACITY]
Override_Mode: int = 0 # 1 = 已出现 --path(强制替换)
Override_Count: int = 0
# 表读取接口(其它模块只经此读取,不直接触碰 Common_Directory)
def DirCount() -> int:
return Common_Dir_Count
def DirAt(Idx: int) -> str:
return Common_Directory[Idx]
def IsOverride() -> int:
return Override_Mode
# --path 首次出现时调用(幂等:仅第一次清空覆盖项);之后不再自动建表
def BeginOverride():
global Override_Mode, Override_Count
if Override_Mode == 0:
Override_Mode = 1
Override_Count = 0
# 收集一条 --path 覆盖项(保序;存在性检查延后到 BuildTable,以便告警落在 [0/3] 段内)
def AddOverridePath(Path: str) -> int:
global Override_Count
if Path is None:
return 0
if Override_Count >= COMMON_DIR_CAPACITY:
VLog.LogFmtIS(" 警告: --path 数量已达上限 (%d),忽略: %s\n", COMMON_DIR_CAPACITY, Path)
return 0
Override_Dir[Override_Count] = Path
Override_Count += 1
return 1
# 无条件入表:仅做 strcmp 去重 + 容量上界,不做存在性检查。返回 1=已加入,0=跳过
# 入参 Path 的生存期须覆盖整个进程(静态字面量或 MemBuddy 池内分配)。
def AddCommonDirForce(Path: str) -> int:
global Common_Dir_Count
if Path is None:
return 0
i: int = 0
while i < Common_Dir_Count:
if string.strcmp(Common_Directory[i], Path) == 0:
return 0
i += 1
if Common_Dir_Count >= COMMON_DIR_CAPACITY:
VLog.LogFmt1(" 警告: 常用目录表已满 (%d),已忽略该条\n", t.CSizeT(COMMON_DIR_CAPACITY))
return 0
Common_Directory[Common_Dir_Count] = Path
Common_Dir_Count += 1
return 1
# 加入常用目录表:存在性检查 + 去重 + 容量上界(扫描/推导出的路径走这条)。
# 返回 1=已加入,0=跳过(不存在 / 重复 / 表满)
def AddCommonDir(Path: str) -> int:
if Path is None:
return 0
if VFs.PathIsDir(Path) == 0:
return 0
return AddCommonDirForce(Path)
# 枚举存储盘的 X:\Program Files 与 X:\Program Files (x86)。返回本次新增条目数
def AddFixedDriveProgramFiles() -> int:
# 进程级设置:抑制"请插入磁盘"等临界错误对话框,并让无介质驱动器立即返回错误,
# 避免探测空读卡器/光驱时长时间阻塞。副作用:影响本进程后续所有文件操作,
# 且本进程生命周期内持续有效(进程级、非线程级、不自动恢复)。
SetErrorMode(SEM_FAILCRITICALERRORS)
Mask: DWORD = GetLogicalDrives()
if Mask == 0:
VLog.LogFmt1(" 警告: GetLogicalDrives 失败 (win32 err=%u)\n", wb.GetLastError())
return 0
Added: int = 0
Letter: int = 0
while Letter < 26:
if (Mask & (t.CUInt32T(1) << t.CUInt32T(Letter))) != 0:
Root: bytes = _mbuddy.alloc(DRIVE_ROOT_CAP)
if Root is not None:
Root[0] = t.CChar(65 + Letter) # 'A' + Letter
Root[1] = ':'
Root[2] = '\\'
Root[3] = 0
Dt: UINT = GetDriveTypeA(Root)
# 决策:只跳过 NO_ROOT_DIR(1) 与 CDROM(5);REMOVABLE(2)/FIXED(3)/
# REMOTE(4) 都继续探测 Program Files(空介质由上方 SetErrorMode 抑制弹窗)。
if Dt != DRIVE_NO_ROOT_DIR and Dt != DRIVE_CDROM:
Pf: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
if Pf is not None:
string.strcpy(Pf, r"Q:\Program Files") # 'Q' 占位,下句改写为首字母
Pf[0] = t.CChar(65 + Letter)
Added += AddCommonDir(str(Pf))
Pfx: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
if Pfx is not None:
string.strcpy(Pfx, r"Q:\Program Files (x86)")
Pfx[0] = t.CChar(65 + Letter)
Added += AddCommonDir(str(Pfx))
Letter += 1
return Added
# 桌面兜底:%USERPROFILE%\Desktop(仅当 SHGetKnownFolderPath 不可用/失败时走)
# Buf 由调用方提供(>= PATH_BUF_CAP)。返回 1=已加入,0=未加入
def AddDesktopFallback(Buf: bytes) -> int:
n: DWORD = GetEnvironmentVariableA("USERPROFILE", Buf, VFs.PATH_BUF_CAP)
# 还需追加 "\Desktop"(8 字节)+ NUL,n + 9 必须不超过缓冲容量
if n == 0 or n + t.CUInt32T(DESKTOP_SUFFIX_CAP) > t.CUInt32T(VFs.PATH_BUF_CAP):
VLog.LogFmt1(" 桌面兜底: 获取 USERPROFILE 失败或过长 (win32 err=%u)\n", wb.GetLastError())
return 0
Buf[n] = 0
string.strcat(Buf, r"\Desktop")
VLog.VerboseFmtS1(" 桌面兜底: 使用 %s\n", str(Buf))
if AddCommonDir(str(Buf)) != 0:
return 1
VLog.LogStr(" 警告: 桌面兜底路径不存在或非目录,未加入常用目录表\n")
return 0
# 当前用户桌面:优先 SHGetKnownFolderPath(FOLDERID_Desktop)(能正确处理
# OneDrive 等重定向场景),失败则显式退回 %USERPROFILE%\Desktop(不静默)。
# 返回 1=已加入,0=未加入(含"不存在"如实报告)
def AddCurrentUserDesktop() -> int:
Buf: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
if Buf is None:
VLog.LogStr(" 警告: 桌面路径缓冲区分配失败\n")
return 0
# KNOWNFOLDERID 即 GUID;内存布局(小端):Data1(u32 LE) / Data2(u16 LE) /
# Data3(u16 LE) / Data4[8] 原样。FOLDERID_Desktop =
# {B4BFCC3A-DB2C-424C-B029-7FE99A87C641} ⇒ 16 字节:
# 3A CC BF B4 2C DB 4C 42 B0 29 7F E9 9A 87 C6 41
# 字段宽度依据:wb.GUID 的 Data1: DWORD / Data2: WORD / Data3: WORD
# (includes/w32/win32base.vp:50-54)+ Microsoft GUID 序列化规范。
# 不复用 wb.GUID:它把 Data4 建模为 BYTEPTR(指针),在 x64 上结构体虽同为 16 字节,
# 但偏移 8..15 存放的是指针值而非 GUID 的 Data4,内容错误 ⇒ 这里用裸 16 字节缓冲。
GuidBuf: BYTEPTR = stdlib.malloc(KNOWNFOLDERID_SIZE)
if GuidBuf is None:
VLog.LogStr(" 警告: KNOWNFOLDERID 缓冲分配失败,改用兜底路径\n")
return AddDesktopFallback(Buf)
GuidBuf[0] = 0x3A
GuidBuf[1] = 0xCC
GuidBuf[2] = 0xBF
GuidBuf[3] = 0xB4
GuidBuf[4] = 0x2C
GuidBuf[5] = 0xDB
GuidBuf[6] = 0x4C
GuidBuf[7] = 0x42
GuidBuf[8] = 0xB0
GuidBuf[9] = 0x29
GuidBuf[10] = 0x7F
GuidBuf[11] = 0xE9
GuidBuf[12] = 0x9A
GuidBuf[13] = 0x87
GuidBuf[14] = 0xC6
GuidBuf[15] = 0x41
Pwsz: WCHARPTR = None
Hr: HRESULT = SHGetKnownFolderPath(t.CPtr(GuidBuf), 0, None, t.CPtr(c.Addr(Pwsz)))
stdlib.free(t.CPtr(GuidBuf))
if Hr < 0:
# 规范上失败时 *ppszPath 会被置 NULL;仍防御性释放非空返回值,杜绝泄漏
if Pwsz is not None:
CoTaskMemFree(t.CPtr(Pwsz))
VLog.LogFmt1(" 桌面: SHGetKnownFolderPath 失败 (hr=0x%X),改用兜底路径\n", t.CSizeT(t.CUInt32T(Hr)))
return AddDesktopFallback(Buf)
if Pwsz is None:
VLog.LogStr(" 桌面: SHGetKnownFolderPath 返回空指针,改用兜底路径\n")
return AddDesktopFallback(Buf)
# UTF-16 → UTF-8:先问所需字节数(含结尾 NUL;Needed<=0 表示失败)
Needed: int = VFs.WideToUtf8Needed(Pwsz)
if Needed <= 0 or Needed > VFs.PATH_BUF_CAP:
CoTaskMemFree(t.CPtr(Pwsz))
VLog.LogFmt2(" 桌面: 路径为空或过长 (%d 字节, 上限 %d),改用兜底路径\n",
t.CSizeT(t.CUInt32T(Needed)), t.CSizeT(VFs.PATH_BUF_CAP))
return AddDesktopFallback(Buf)
Got: int = VFs.WideToUtf8(Pwsz, Buf, VFs.PATH_BUF_CAP)
CoTaskMemFree(t.CPtr(Pwsz)) # 用后立即释放:成功/失败分支的同一释放点
if Got == 0:
VLog.LogFmt1(" 桌面: UTF-16→UTF-8 转换失败 (win32 err=%u),改用兜底路径\n", wb.GetLastError())
return AddDesktopFallback(Buf)
if AddCommonDir(str(Buf)) != 0:
return 1
VLog.LogStr(" 警告: 桌面路径不存在或非目录,未加入常用目录表\n")
return 0
# 打印常用目录表(逐条列举与合计属过程细节,仅 --verbose 时输出)。返回 0=表非空,1=表为空
def PrintCommonDirs() -> int:
if Common_Dir_Count <= 0:
VLog.VerboseStr(" (表为空)\n")
return 1
i: int = 0
while i < Common_Dir_Count:
VLog.VerboseFmtIS(" [%d] %s\n", t.CSizeT(i), Common_Directory[i])
i += 1
VLog.VerboseFmt1(" 合计 %d 个常用目录\n", t.CSizeT(Common_Dir_Count))
return 0
# 构建并打印目录表(须在 _mbuddy 注入后调用)。返回 0=表非空,1=表为空。
# 需求 2:日志显式区分"自动表" / "覆盖表(N 条)";覆盖模式下不存在的 --path 告警并跳过。
def BuildTable() -> int:
if Override_Mode == 0:
AddCommonDirForce(r"C:\Viper") # 静态两条:无条件入表(不做存在性检查)
AddCommonDirForce(r"D:\Viper")
AddFixedDriveProgramFiles() # 各盘 Program Files / Program Files (x86)
AddCurrentUserDesktop() # 当前用户桌面
VLog.VerboseFmt1(" 目录表来源: 自动表 (%u 条)\n", t.CSizeT(Common_Dir_Count))
else:
# 强制替换:完全不做自动构建,表 = --path 序列(保序;去重由 AddCommonDirForce)
i: int = 0
while i < Override_Count:
if VFs.PathIsDir(Override_Dir[i]) == 0:
VLog.LogFmtS1(" 警告: --path 不存在或非目录,跳过: %s\n", Override_Dir[i])
else:
AddCommonDirForce(Override_Dir[i])
i += 1
VLog.VerboseFmt1(" 目录表来源: 覆盖表 (%u 条)\n", t.CSizeT(Common_Dir_Count))
return PrintCommonDirs()
+324
View File
@@ -0,0 +1,324 @@
import t, c
from stdint import *
import stdlib
import string
import memhub
import w32.win32file as wf
import w32.win32base as wb
# ============================================================
# VFs - 路径 / 文件 / 通用字节串与编码工具
#
# 从原 App/main.vp 平移:
# * 通用字节串:CopyRaw / DupStr / StrEqNoCase
# * 路径:JoinPath / PathIsDir / PathIsFile
# * 编码:Utf8ToAnsi / AnsiToUtf8(+ 本轮新增 WideToUtf8Needed/WideToUtf8)
# * 文件:ReadFull(+ 本轮抽出的 ReadWholeFile / WriteFileAll)
#
# 原 SeekTo(h, Off) 已删除:它以 `wf.SetFilePointerEx(h, t.CPtr(c.Addr(li)), None, wf.FILE_BEGIN)`
# 传 liDistanceToMove,而真实 Win32 该参数按值传 8 字节(传地址会让"距离"变成该地址的
# 数值,文件指针被推到 EOF 之后,随后 ReadFile 只读回 0 字节)。该函数在当前源码中
# 零调用点:VPe 需要段内容时改走整文件读(VPe.vp:283-297),ver [1/3] 也是整文件读
# (VPe.vp:320-321)。需要定位读取时改用 32 位 wf.SetFilePointer(按值 LONG),
# 先例 includes/w32/fileio.vp:169。
#
# 依赖本仓既有符号:
# wf.GetFileAttributesA(includes/w32/win32file.vp:99)、wf.FILE_ATTRIBUTE_DIRECTORY(:23)、
# wf.CreateFileA(:88)、wf.GetFileSizeEx(:95)、wf.ReadFile(:90)、wf.WriteFile(:91)、
# wf.GENERIC_READ(:5)、wf.GENERIC_WRITE(:6)、
# wf.FILE_SHARE_READ/WRITE/DELETE(:10-12)、wf.OPEN_EXISTING(:16)、wf.CREATE_ALWAYS(:14)、
# wf.FILE_ATTRIBUTE_NORMAL(:25)、wb.GetLastError(:62)、wb.CloseHandle(:64)、
# wb.MultiByteToWideChar(:94)、wb.LARGE_INTEGER(:56)、wb.INVALID_HANDLE_VALUE(:9)。
#
# Win32 自声明(本仓无声明;x64 统一调用约定,无需修饰):
# kernel32!WideCharToMultiByte: int WideCharToMultiByte(UINT,DWORD,LPCWCH,int,LPSTR,int,LPCCH,LPBOOL);
# 原型出处:Win32 API 文档 / winnls.h。
# ============================================================
PATH_BUF_CAP: t.CDefine = 512 # 单条目录/文件路径缓冲
CONV_CAP: t.CDefine = 2048 # 编码转换临时缓冲
INVALID_FILE_ATTRIBUTES: t.CDefine = 0xFFFFFFFF # GetFileAttributesA 失败返回值
CP_UTF8: t.CDefine = 65001 # Win32 代码页 UTF-8
CP_ACP: t.CDefine = 0 # Win32 代码页 ANSI(A 系列 API 的当前页)
extern def WideCharToMultiByte(CodePage: UINT, dwFlags: DWORD, lpWideCharStr: WCHARPTR,
cchWideChar: int, lpMultiByteStr: CHARPTR, cbMultiByte: int,
lpDefaultChar: wb.LPCSTR, lpUsedDefaultChar: BOOL | t.CPtr) -> int
# 由 App/main.vp 注入(与其它模块同一 MemBuddy 实例)
_mbuddy: rawptr[memhub.MemBuddy]
# ReadWholeFile / WriteFileAll 的结果输出(模块全局)
LastReadSize: t.CSizeT = 0 # 成功时的实际读取字节数
LastReadErr: int = 0 # 1=打开失败 2=取大小失败 3=空文件 4=超过上限 5=分配失败
LastReadWinErr: ULONG = 0 # 打开失败时的 GetLastError()
LastWriteDone: t.CSizeT = 0 # WriteFileAll 实际写入字节数
# ============================================================
# 通用字节串
# ============================================================
# 复制 Src 到 Dst(含结尾 NUL),容量不足返回 0(不写穿)
def CopyRaw(Dst: bytes, Cap: int, Src: str) -> int:
if Dst is None:
return 0
if Cap <= 0:
return 0
if Src is None:
Dst[0] = 0
return 0
n: t.CSizeT = string.strlen(Src)
if n + 1 > t.CSizeT(Cap):
Dst[0] = 0
return 0
i: t.CSizeT = 0
while i < n:
Dst[i] = Src[i]
i += 1
Dst[n] = 0
return 1
# 用主池复制出 Src 的独立副本(返回 None 表示失败)。
# 释放用 VFs._mbuddy.free(模块间共享同一池实例,见 main.vp 注入)。
def DupStr(S: str) -> str:
if S is None:
return None
n: t.CSizeT = string.strlen(S)
b: bytes = _mbuddy.alloc(n + 1)
if b is None:
return None
i: t.CSizeT = 0
while i < n:
b[i] = S[i]
i += 1
b[n] = 0
return str(b)
# ASCII 大小写无关比较(Windows 文件名不区分大小写)
def StrEqNoCase(A: str, B: str) -> int:
i: int = 0
while True:
ca: int = int(A[i])
cb: int = int(B[i])
if ca >= 65 and ca <= 90:
ca += 32
if cb >= 65 and cb <= 90:
cb += 32
if ca != cb:
return 0
if ca == 0:
return 1
i += 1
# ============================================================
# 路径
# ============================================================
# 拼接 Parent + "\" + Child 到 Out(容量 Cap),返回 1=成功,0=溢出
# 说明:includes/os/path.vp:88 的 join 用模块全局 path._mbuddy 分配(本进程未注入 ⇒ 空指针),
# 且无显式容量上界。故此处自拼并做上界判断。
def JoinPath(Out: bytes, Cap: int, Parent: str, Child: str) -> int:
if Out is None or Parent is None or Child is None:
return 0
pl: t.CSizeT = string.strlen(Parent)
cl: t.CSizeT = string.strlen(Child)
need_sep: int = 0
if pl > 0:
last: int = int(Parent[t.CInt(pl) - 1])
if last != 47 and last != 92: # '/' or '\'
need_sep = 1
total: t.CSizeT = pl + cl + t.CSizeT(need_sep)
if total + 1 > t.CSizeT(Cap):
return 0
i: t.CSizeT = 0
while i < pl:
Out[i] = Parent[i]
i += 1
if need_sep != 0:
Out[i] = '\\'
i += 1
j: t.CSizeT = 0
while j < cl:
Out[i] = Child[j]
i += 1
j += 1
Out[i] = 0
return 1
# 路径是否为"已存在的目录":GetFileAttributesA 返回 != INVALID 且含 DIRECTORY 位
def PathIsDir(Path: str) -> int:
Attrs: ULONG = wf.GetFileAttributesA(Path)
if Attrs == INVALID_FILE_ATTRIBUTES:
return 0
if (Attrs & wf.FILE_ATTRIBUTE_DIRECTORY) != 0:
return 1
return 0
# 路径是否为"已存在的普通文件"(存在且非目录)
def PathIsFile(Path: str) -> int:
Attrs: ULONG = wf.GetFileAttributesA(Path)
if Attrs == INVALID_FILE_ATTRIBUTES:
return 0
if (Attrs & wf.FILE_ATTRIBUTE_DIRECTORY) != 0:
return 0
return 1
# ============================================================
# 编码转换(A 系列 API 走系统 ANSI;Viper 字符串约定 UTF-8)
# ============================================================
# UTF-8 → ANSI(失败/非法 UTF-8 时原样拷贝,视作已是 ANSI)
def Utf8ToAnsi(Src: str, Dst: bytes, Cap: int) -> int:
if Src is None:
Dst[0] = 0
return 0
n: int = wb.MultiByteToWideChar(CP_UTF8, 0, Src, -1, t.CPtr(0), 0)
if n <= 0:
CopyRaw(Dst, Cap, Src)
return 0
W: WCHARPTR = stdlib.malloc(t.CSizeT(n) * 2)
if W is None:
CopyRaw(Dst, Cap, Src)
return 0
wb.MultiByteToWideChar(CP_UTF8, 0, Src, -1, W, n)
got: int = WideCharToMultiByte(CP_ACP, 0, W, -1, Dst, Cap, None, None)
stdlib.free(t.CPtr(W))
if got <= 0:
CopyRaw(Dst, Cap, Src)
return 0
return 1
# ANSI → UTF-8(失败时原样拷贝)
def AnsiToUtf8(Src: str, Dst: bytes, Cap: int) -> int:
if Src is None:
Dst[0] = 0
return 0
n: int = wb.MultiByteToWideChar(CP_ACP, 0, Src, -1, t.CPtr(0), 0)
if n <= 0:
CopyRaw(Dst, Cap, Src)
return 0
W: WCHARPTR = stdlib.malloc(t.CSizeT(n) * 2)
if W is None:
CopyRaw(Dst, Cap, Src)
return 0
wb.MultiByteToWideChar(CP_ACP, 0, Src, -1, W, n)
got: int = WideCharToMultiByte(CP_UTF8, 0, W, -1, Dst, Cap, None, None)
stdlib.free(t.CPtr(W))
if got <= 0:
CopyRaw(Dst, Cap, Src)
return 0
return 1
# UTF-16 → UTF-8 所需字节数(含结尾 NUL);<=0 表示失败
def WideToUtf8Needed(Src: WCHARPTR) -> int:
if Src is None:
return 0
return WideCharToMultiByte(CP_UTF8, 0, Src, -1, t.CPtr(0), 0, None, None)
# UTF-16 → UTF-8(写入 Dst,容量 Cap),返回 1=成功
def WideToUtf8(Src: WCHARPTR, Dst: bytes, Cap: int) -> int:
if Src is None or Dst is None:
return 0
if WideCharToMultiByte(CP_UTF8, 0, Src, -1, Dst, Cap, None, None) <= 0:
return 0
return 1
# ============================================================
# 文件读写
# ============================================================
# 从当前文件指针读满 Want 字节(循环),返回实际读取字节数
def ReadFull(h: wb.HANDLE, Buf: bytes, Want: t.CSizeT) -> t.CSizeT:
total: t.CSizeT = 0
while total < Want:
chunk: t.CSizeT = Want - total
if chunk > 0x7FFFFFFF:
chunk = t.CSizeT(0x7FFFFFFF)
br: ULONG = 0
if wf.ReadFile(h, Buf + total, ULONG(chunk), t.CPtr(c.Addr(br)), None) == 0:
break
if br == 0:
break
total += t.CSizeT(br)
return total
# 读取整个文件到 stdlib 分配的缓冲(含 NUL 结尾)。成功返回 bytes,调用方用 stdlib.free 释放;
# 失败返回 None,原因见 LastReadErr(4 仅在 MaxBytes>0 时产生)。
# Share 由调用方给出(各调用点原有的 dwShareMode 语义保持一致)。
def ReadWholeFile(Path: str, MaxBytes: t.CSizeT, Share: ULONG) -> bytes:
global LastReadSize, LastReadErr, LastReadWinErr
LastReadSize = 0
LastReadErr = 0
LastReadWinErr = 0
H: wb.HANDLE = wf.CreateFileA(Path, wf.GENERIC_READ, Share,
None, wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, None)
if H == wb.INVALID_HANDLE_VALUE:
LastReadWinErr = wb.GetLastError()
LastReadErr = 1
return None
li: wb.LARGE_INTEGER = wb.LARGE_INTEGER()
if wf.GetFileSizeEx(H, t.CPtr(c.Addr(li))) == 0:
wb.CloseHandle(H)
LastReadErr = 2
return None
Sz: t.CSizeT = t.CSizeT(li.QuadPart)
if Sz <= 0:
wb.CloseHandle(H)
LastReadErr = 3
return None
if MaxBytes > 0 and Sz > MaxBytes:
wb.CloseHandle(H)
LastReadErr = 4
return None
Buf: bytes = stdlib.malloc(Sz + 1)
if Buf is None:
wb.CloseHandle(H)
LastReadErr = 5
return None
Got: t.CSizeT = ReadFull(H, Buf, Sz)
wb.CloseHandle(H)
Buf[Got] = 0
LastReadSize = Got
return Buf
# 把 Buf[0..Len) 全量写入 Path(CREATE_ALWAYS)。
# 返回 1=完整写入,0=打开/写入失败,-1=写入不完整(实际字节见 LastWriteDone)
def WriteFileAll(Path: str, Buf: bytes, Len: t.CSizeT) -> int:
global LastWriteDone
LastWriteDone = 0
if Buf is None:
return 0
H: wb.HANDLE = wf.CreateFileA(Path, wf.GENERIC_WRITE, wf.FILE_SHARE_READ,
None, wf.CREATE_ALWAYS, wf.FILE_ATTRIBUTE_NORMAL, None)
if H == wb.INVALID_HANDLE_VALUE:
return 0
total: t.CSizeT = 0
while total < Len:
chunk: t.CSizeT = Len - total
if chunk > 0x7FFFFFFF:
chunk = t.CSizeT(0x7FFFFFFF)
bw: ULONG = 0
if wf.WriteFile(H, Buf + total, ULONG(chunk), t.CPtr(c.Addr(bw)), None) == 0:
break
if bw == 0:
break
total += t.CSizeT(bw)
wb.CloseHandle(H)
LastWriteDone = total
if total < Len:
return -1
return 1
+1279
View File
File diff suppressed because it is too large. Load diff
+54
View File
@@ -0,0 +1,54 @@
import t, c
from stdint import *
import memhub
import viperlib
import lib.VPe as VPe
# ============================================================
# VGuid - UUID(ole32!CoCreateGuid → 8-4-4-4-12)
#
# 从原 App/main.vp 平移 NewGuidStr 及 CoCreateGuid 自声明。
# 依赖:VPe.ReadU16/ReadU32(PE 小端读取,includes 无对应辅助)、VFs 无直接依赖。
#
# Win32 自声明(本仓无声明):
# ole32!CoCreateGuid: HRESULT CoCreateGuid(GUID *pguid); (objbase.h;不依赖 CoInitialize)
# HRESULT 为有符号 32 位(失败为负),与 includes/w32/win32base.vp:5 的 HANDLE 无关。
# DLL 归属:ole32(project.vpj 已含 -lole32)。
# ============================================================
GUID_SIZE: t.CDefine = 16 # GUID:Data1(4)+Data2(2)+Data3(2)+Data4(8)
GUID_STR_CAP: t.CDefine = 40 # "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + NUL
HRESULT: t.CTypedef = t.CInt32T # Win32: typedef LONG HRESULT(负值才是失败)
extern def CoCreateGuid(pguid: t.CPtr) -> HRESULT
# 由 App/main.vp 注入
_mbuddy: rawptr[memhub.MemBuddy]
# 生成 UUID 字符串(8-4-4-4-12,小写十六进制)写入 Out;返回 1=成功。
# Data1/Data2/Data3 在内存中小端存放:按字段读回数值后再按 %08x/%04x 文本化,
# Data4[8] 原样按字节文本化。
def NewGuidStr(Out: bytes, Cap: int) -> int:
GB: bytes = _mbuddy.alloc(GUID_SIZE)
if GB is None:
return 0
Hr: HRESULT = CoCreateGuid(t.CPtr(GB))
if Hr < 0:
_mbuddy.free(t.CPtr(GB))
return 0
ub: BYTEPTR = BYTEPTR(GB)
D1: t.CUInt32T = VPe.ReadU32(ub, 0)
D2: t.CUInt32T = VPe.ReadU16(ub, 4)
D3: t.CUInt32T = VPe.ReadU16(ub, 6)
viperlib.snprintf(Out, t.CSizeT(Cap),
"%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
D1, D2, D3,
t.CUInt32T(ub[8]), t.CUInt32T(ub[9]),
t.CUInt32T(ub[10]), t.CUInt32T(ub[11]),
t.CUInt32T(ub[12]), t.CUInt32T(ub[13]),
t.CUInt32T(ub[14]), t.CUInt32T(ub[15]))
_mbuddy.free(t.CPtr(GB))
return 1
+258
View File
@@ -0,0 +1,258 @@
import t, c
from stdint import *
import stdlib
import memhub
import string
import json
import lib.VFs as VFs
import lib.VLog as VLog
import lib.VCache as VCache
# ============================================================
# VJson - JSON 构建(单条对象 / 整轮数组,全部走仓库 json 库)
#
# 本轮改造(需求 ①/②):
# * 不再手工拼 `{"k":"v",...}`、不再手写 JsonEscape:条目改为用库的
# json.object + json.object_set + json.string_val 构造,再由 json.write 序列化;
# 转义(\ " \n \r \t)与 UTF-8 原样透传由库保证(includes/json/__writer.vp:39-66)。
# * 输出为**紧凑单行**:库的 write 忽略 pretty 参数、且从不写空白/换行
# (includes/json/__writer.vp:69-137、:151-155),故整轮数组与其中每个对象在同一行。
# * 数组框架('[' / ',' / ']' / 结尾换行)仍由本模块拼接:库的 write 只写单个输出缓冲
# (缺省 cap = 4096,includes/json/__writer.vp:143-148),整轮数组(最多 256 条)
# 无法一次交给库写,故逐条 write 再拼接。
#
# 库接口(静态核验,详见交付报告「能力与用法清单」):
# json.object(pool) -> rawptr[JsonValue]
# json.string_val(pool, s: str) -> rawptr[JsonValue] # 内部复制一份字符串
# json.object_set(pool, obj, key: str, val) -> 无返回 # 追加序 = 输出键序
# json.write(pool, val, pretty: bool, cap: t.CSizeT = 4096) -> str
# # pretty 被忽略,恒紧凑单行;超出 cap 返回 None
# json.parse(pool, src: str) -> rawptr[JsonValue]
# pool 形参类型 rawptr[memhub.MemManager](vtable 基类)⇒ MemBuddy / MemPool 都可传
# (先例:includes/requests/https.vp:823,883 传 rawptr[memhub.MemPool])。
#
# 用途约定:本模块声明的 Json_Buf 同时供 stdout 与实际写盘的缓存文件使用
# (调用点见 VScan.RunScanRound),二者逐字节相同。
#
# 失败处理(fail-closed):条目在分配前先过 Json_Pool.AllocOk(Need) 预检(BuildEntry),
# 预检不足即跳过该条目(返回 None);逐条跳过原因(含路径)仅 --verbose 输出,默认由
# EmitJson 的轮末聚合("本轮有 N 条被跳过")给出计数——库内部的 pool.alloc 不会在本模块
# 路径上走到 includes/memhub.vp:262-272 的 printf + exit(1);跳过不写 Json_Buf,
# 故本轮数组仍是合法单行 JSON(末尾 ']' 与换行照常闭合)。
#
# 依赖:VCache(CacheCount/CacheExe… 读表)、VFs(AnsiToUtf8/CopyRaw)、VLog。
#
# 内存归属(进程生命周期,均不释放):
# * Json_Buf / Conv_Buf 由 Vet_Mb(main 注入的 MemBuddy)分配;
# * Json_Arena 由 stdlib.malloc 分配;
# * Json_Pool(MemPool 管理器对象)由 memhub.NewMemPool 在堆上就地构造
# ——JsonInit 的栈帧在返回后失效,管理器对象必须在堆上(见 memhub.vp 的
# NewMemPool 注释与 Debugger/memhub-lifetime.md)。
# ============================================================
JSON_BUF_CAP: t.CDefine = 1048576 # 一轮 JSON 数组缓冲 1 MiB
JSON_POOL_CAP: t.CDefine = 65536 # 逐条序列化工作池 64 KiB(每条目 reset 复用)
JSON_ENTRY_MAX: t.CDefine = 4096 # 单条上限 = json 库 write 的缺省 cap(4096)
JSON_ENTRY_SLACK: t.CDefine = 512 # 单条固定语法开销上界(实测 77:键/值引号 24 + 键名 40 + 冒号 6 + 逗号 5 + 大括号 2)
# 单条序列化在 Json_Pool 上的固定开销上界(BuildEntry 的 AllocOk 预检的一部分)。
# 组成(一次 BuildEntry 的全部池分配;条目级 reset 之后 offset=0):
# * 7 个 JsonValue 节点(1 个对象 + 6 个字符串值)× 单节点上界 72 字节 = 504
# (JsonValue 9 字段 × 8 字节上界,includes/json/__init__.vp:23-39,与 VCache
# 解析树的估算口径一致);
# * 6 个键名副本(含 NUL)46 字节:id 3 + executable 11 + version 8 + arch 5
# + source 7 + displayName 12(includes/json/__init__.vp:225-229);
# * json.write 的输出缓冲 4096 字节(includes/json/__writer.vp:143-148,缺省 cap);
# * 余量 512 字节:6 个值副本各带一个结尾 NUL,加条内 20 次池分配按
# MEMHUB_ALIGN=8 向上取整的浪费(每次 ≤7,合计 ≤140)。
# 合计 504 + 46 + 4096 + 512 = 5158。
JSON_ENTRY_POOL_FIXED: t.CDefine = 5158
# 单条里 executable / displayName 的取值长度上界:二者都由 Conv_Buf(VFs.CONV_CAP 字节)
# 拷出(WideCharToMultiByte 以 Cap 封顶并写结尾 NUL,失败时 VFs.CopyRaw 写空串),
# 故每个值 ≤ CONV_CAP-1 字符。写 2047 = VFs.CONV_CAP(2048) - 1 的字面量:模块级
# t.CDefine 不引用跨模块常量,VFs.CONV_CAP 只在函数体内参与算术。
JSON_ENTRY_CONV_MAX: t.CDefine = 2047
# 由 App/main.vp 注入
_mbuddy: rawptr[memhub.MemBuddy]
Json_Buf: bytes = None # 整轮数组文本(stdout 与缓存文件共用)
Json_Len: int = 0 # Json_Buf 的有效字节数
Conv_Buf: bytes = None # ANSI → UTF-8 转换缓冲
Json_Arena: bytes = None # 序列化工作池 arena(stdlib.malloc,常驻)
Json_Pool: rawptr[memhub.MemPool] # 序列化工作池(堆上就地构造;见 JsonInit)
# 分配 JSON 静态缓冲与序列化工作池(进程生命周期内复用)。返回 1=成功,0=失败
def JsonInit() -> int:
global Json_Buf, Conv_Buf, Json_Arena, Json_Pool
Json_Buf = _mbuddy.alloc(JSON_BUF_CAP)
Conv_Buf = _mbuddy.alloc(VFs.CONV_CAP)
Json_Arena = stdlib.malloc(JSON_POOL_CAP)
if Json_Buf is None or Conv_Buf is None or Json_Arena is None:
return 0
# 池对象走堆上就地构造(memhub.NewMemPool):Json_Pool 要被 EmitJson/BuildEntry
# 跨帧使用,而本函数的栈帧在返回时即失效——构造表达式在这里会把池对象的存储
# 落在本帧栈上(IR:VJson 的 alloca %MemPool + __before_init__ + __init__)。
# arena 由 stdlib.malloc 常驻;池对象同样常驻,进程生命周期内不 free。
Pool: rawptr[memhub.MemPool] = memhub.NewMemPool(Json_Arena, JSON_POOL_CAP)
if Pool is None:
return 0
Json_Pool = Pool
return 1
# 供 VScan 写 stdout / VCache 写缓存文件读取结果
def JsonBuf() -> bytes:
return Json_Buf
def JsonLen() -> int:
return Json_Len
def JReset():
global Json_Len
Json_Len = 0
if Json_Buf is not None:
Json_Buf[0] = 0
# 把字面量/已序列化文本追加到 Json_Buf(容量不足返回 0,不写穿)
def JWrite(S: str) -> int:
global Json_Len
if S is None or Json_Buf is None:
return 0
n: t.CSizeT = string.strlen(S)
if t.CSizeT(Json_Len) + n + 1 > t.CSizeT(JSON_BUF_CAP):
return 0
i: t.CSizeT = 0
while i < n:
Json_Buf[Json_Len + t.CInt(i)] = S[i]
i += 1
Json_Len += t.CInt(n)
Json_Buf[Json_Len] = 0
return 1
# 构造第 Idx 条 JSON 对象并交给库序列化为紧凑单行文本。
# 返回该条文本(分配在 Json_Pool 上,长度 < JSON_ENTRY_MAX);None=已跳过。
# 每条跳过的具体原因(含被处理路径)仅在 --verbose 时逐条给出;默认安静模式下由
# EmitJson 的轮末聚合("本轮有 N 条被跳过")给出计数,既不静默异常也不逐路径刷屏。
def BuildEntry(Idx: int) -> str:
# 池容量预检(fail-closed):本条在 Json_Pool 上的全部分配必须先确认放得下,
# 否则库内部的 pool.alloc 会走到 includes/memhub.vp:262-272 的 printf + exit(1)。
# 预检在 EmitJson 的条目级 reset 之后执行(池 offset=0),故单次 AllocOk 覆盖整条;
# Need 由固定开销 + 全部值的长度上界构成,是条内池占用(含 20 次分配的 8 字节对齐
# 浪费)的上界,推导见 JSON_ENTRY_POOL_FIXED 与 JSON_ENTRY_CONV_MAX 的注释。
ValNeed: t.CSizeT = t.CSizeT(JSON_ENTRY_CONV_MAX) + t.CSizeT(JSON_ENTRY_CONV_MAX)
ValNeed = ValNeed + string.strlen(VCache.CacheId(Idx))
ValNeed = ValNeed + string.strlen(VCache.CacheVer(Idx))
ValNeed = ValNeed + string.strlen(VCache.CacheArch(Idx))
ValNeed = ValNeed + string.strlen(VCache.CacheSrc(Idx)) # source("workspace"/"check")
Need: t.CSizeT = t.CSizeT(JSON_ENTRY_POOL_FIXED) + ValNeed
if Json_Pool.AllocOk(Need) == 0:
VLog.VerboseFmtIS(" 警告: JSON 工作池不足(本条需 %zu 字节),已跳过: %s\n",
Need, VCache.CacheExe(Idx))
return None
Obj: rawptr[json.JsonValue] = json.object(Json_Pool)
if Obj == None:
VLog.VerboseStr(" 警告: JSON 对象分配失败(工作池不足),已跳过该条目\n")
return None
# 键插入顺序 = 输出顺序(库按 child→next 链表输出,见 __writer.vp:92-107)。
# executable / displayName 按既有约定转 UTF-8,且**转换后立即交给库复制**:
# Conv_Buf 随后要复用于 displayName,晚一步用就会让 executable 串被覆盖。
json.object_set(Json_Pool, Obj, "id", json.string_val(Json_Pool, VCache.CacheId(Idx)))
if VFs.AnsiToUtf8(VCache.CacheExe(Idx), Conv_Buf, VFs.CONV_CAP) == 0:
VFs.CopyRaw(Conv_Buf, VFs.CONV_CAP, VCache.CacheExe(Idx))
json.object_set(Json_Pool, Obj, "executable", json.string_val(Json_Pool, str(Conv_Buf)))
json.object_set(Json_Pool, Obj, "version", json.string_val(Json_Pool, VCache.CacheVer(Idx)))
json.object_set(Json_Pool, Obj, "arch", json.string_val(Json_Pool, VCache.CacheArch(Idx)))
# source 取自缓存条目(扫盘="workspace";check 通道="check",见 VCache.Cache_Src)
json.object_set(Json_Pool, Obj, "source", json.string_val(Json_Pool, VCache.CacheSrc(Idx)))
if VFs.AnsiToUtf8(VCache.CacheDisp(Idx), Conv_Buf, VFs.CONV_CAP) == 0:
VFs.CopyRaw(Conv_Buf, VFs.CONV_CAP, VCache.CacheDisp(Idx))
json.object_set(Json_Pool, Obj, "displayName", json.string_val(Json_Pool, str(Conv_Buf)))
# 结构自检:库在各分配点"失败即不追加/字段留空",此处逐字段确认 6 个键都带上了
# 键名与字符串值(池耗尽时若漏检会写出 `{null:"…"}` 这类非法 JSON)。
# 顺带累计各值长度,供容量守卫使用。
Ok: int = 1
N: int = 0
Sum: t.CSizeT = 0
cu: rawptr[json.JsonValue] = Obj.child
while cu != None:
if cu.key == None or cu.vtype != json.JSON_STRING or cu.str_val == None:
Ok = 0
else:
Sum = Sum + string.strlen(cu.str_val)
N += 1
cu = cu.next
if Ok == 0 or N != 6:
VLog.VerboseStr(" 警告: JSON 条目构造不完整(工作池不足),已跳过该条目\n")
return None
# 容量守卫:库的 write 缺省 cap=4096(=JSON_ENTRY_MAX),超出已会返回 None 且不写穿
# (includes/json/__writer.vp:143-154);此处提前守卫只是避免走到"整条被丢弃 + 告警"。
# 上界推导(Sum = 6 个值长度和):
# 实际输出 = 各值转义长度和 + 固定语法 77(键/值引号 24 + 键名 40 + 冒号 6 + 逗号 5 + 大括号 2)
# ≤ 2*Sum + 77 ≤ 2*Sum + JSON_ENTRY_SLACK(512)
# 故 2*Sum + JSON_ENTRY_SLACK ≤ JSON_ENTRY_MAX ⇒ 实际输出 ≤ 3661 < 4095,含结尾 NUL 也放得下。
if 2 * Sum + t.CSizeT(JSON_ENTRY_SLACK) > t.CSizeT(JSON_ENTRY_MAX):
VLog.VerboseFmtIS(" 警告: JSON 条目过长(超过 %zu 字节),已跳过: %s\n",
t.CSizeT(JSON_ENTRY_MAX), VCache.CacheExe(Idx))
return None
Txt: str = json.write(Json_Pool, Obj, False)
if Txt is None:
VLog.VerboseStr(" 警告: JSON 序列化失败(工作池不足),已跳过该条目\n")
return None
return Txt
# 把当前缓存整体构建为**单行** JSON 数组文本写入 Json_Buf(stdout 与缓存文件共用)
def EmitJson() -> int:
JReset()
if Json_Pool is None:
# 不变式:JsonInit(VScan.ScanInit 调用)成功才会走到 EmitJson;此分支仅为不崩。
VLog.LogStr(" 警告: JSON 工作池未初始化,跳过本轮 JSON 输出\n")
return 0
Truncated: int = 0
Skipped: int = 0
Count: int = 0
if JWrite("[") == 0:
return 0
i: int = 0
while i < VCache.CacheCount():
# 条目级回收:工作池峰值 = 单条(节点 + 字符串副本 + 库的 4096 字节输出缓冲)
Json_Pool.reset()
Txt: str = BuildEntry(i)
if Txt is None:
Skipped += 1
else:
n: t.CSizeT = string.strlen(Txt)
Need: t.CSizeT = n + 3 # 自身 + 结尾 ']' + '\n' + NUL
if Count > 0:
Need = Need + 1 # 分隔逗号
if t.CSizeT(Json_Len) + Need > t.CSizeT(JSON_BUF_CAP):
# 缓冲不足:跳过该条(不再像旧实现那样写出半条),末尾仍闭合 ']'
Truncated = 1
else:
if Count > 0:
JWrite(",")
JWrite(Txt)
Count += 1
i += 1
if JWrite("]") == 0:
Truncated = 1
if JWrite("\n") == 0:
Truncated = 1
if Truncated != 0:
VLog.LogFmt2(" 警告: JSON 输出缓冲不足,本轮已截断(已写 %zu 条 / 缓存 %zu 条)\n",
t.CSizeT(Count), t.CSizeT(VCache.CacheCount()))
if Skipped != 0:
VLog.LogFmt1(" 警告: 本轮有 %zu 条 JSON 条目因过长/工作池不足被跳过\n",
t.CSizeT(Skipped))
return 0
+242
View File
@@ -0,0 +1,242 @@
import t, c
from stdint import *
import stdlib
import string
import viperlib
import w32.win32file as wf
import w32.win32base as wb
# ============================================================
# VLog - 输出原语(全部人类可读日志走 stderr;stdout 只留 JSON)
#
# 本模块从原 App/main.vp 的 [stderr 日志] 段整体平移,行为不变:
# * LogStr/LogFmt1/2/3/LogFmtS1/S2/LogFmtIS 沿用原实现与固定元数转发风格;
# * 本轮新增 LogFmt4(进度行需要 4 个整数参数)与两个时间戳辅助。
#
# 日志详细度(本轮新增):模块级开关 Verbose_Enabled(0=默认安静,1=--verbose)。
# * 默认安静:只保留"启动摘要 / 心跳确认提示与失败原因 / 警告与致命错误 / 轮末一行";
# 过程细节(进度行、扫描根进出、逐条 [reject]/[pe]、逐次 --version 探测、目录表逐条
# 列举、轮末的"下轮开始")一律不输出。
# * VerboseStr / VerboseFmt1/2/3/4 / VerboseFmtS1/S2/VerboseFmtIS 是 Log* 的兄弟函数:
# 开关关闭时直接返回(不格式化、不写 stderr),打开时逐字转发给对应 Log*。
# 调用点只写 Verbose*,"开关判断"集中在本模块,不散落到各调用点。
# * 取值由 VCli 解析 --verbose 后经 SetVerbose 在**任何日志之前**设置
# (scan:App/main.vp:VCli.ParseScanArgs 之后;看门狗:confirm-watch 参数解析之后)。
# 依赖(本仓既有):
# wf.GetStdHandle(includes/w32/win32file.vp:120)、wf.WriteFile(:91)、
# wf.STD_ERROR_HANDLE(:46)、wf.STD_OUTPUT_HANDLE(:45)、wb.INVALID_HANDLE_VALUE(:9)、
# wb.GetLocalTime(includes/w32/win32base.vp:72)、wb.SystemTimeToFileTime(:74)、
# wb.FileTimeToSystemTime(:73)、wb.SYSTEMTIME(:40)、wb.FILETIME(:36)、
# viperlib.snprintf(includes/viperlib.vp:25)、string.strlen(includes/string.vp:69)。
# ============================================================
LOG_CAP: t.CDefine = 4096 # 日志格式化缓冲
Log_Buffer: bytes = None # 日志格式化缓冲(stdlib.malloc,进程生命周期)
# 字面量/已格式化字节串原样写 stderr(不自动追加换行)
def LogStr(S: str):
if S is None:
return
H: wb.HANDLE = wf.GetStdHandle(wf.STD_ERROR_HANDLE)
if H is None or H == wb.INVALID_HANDLE_VALUE:
return
n: t.CSizeT = string.strlen(S)
if n == 0:
return
bw: ULONG = 0
wf.WriteFile(H, S, ULONG(n), t.CPtr(c.Addr(bw)), None)
def LogInit() -> int:
global Log_Buffer
Log_Buffer = stdlib.malloc(LOG_CAP)
if Log_Buffer is None:
return 0
Log_Buffer[0] = 0
return 1
# 固定元数的格式化转发(非 varargs 函数 ⇒ 不引入自定义可变参数风险):
# 内部调用既有的 viperlib.snprintf,把格式化结果写 stderr。
def LogFmt1(Fmt: str, A: t.CSizeT) -> int:
if Log_Buffer is None:
return 0
viperlib.snprintf(Log_Buffer, t.CSizeT(LOG_CAP), Fmt, A)
LogStr(str(Log_Buffer))
return 1
def LogFmt2(Fmt: str, A: t.CSizeT, B: t.CSizeT) -> int:
if Log_Buffer is None:
return 0
viperlib.snprintf(Log_Buffer, t.CSizeT(LOG_CAP), Fmt, A, B)
LogStr(str(Log_Buffer))
return 1
def LogFmt3(Fmt: str, A: t.CSizeT, B: t.CSizeT, C: t.CSizeT) -> int:
if Log_Buffer is None:
return 0
viperlib.snprintf(Log_Buffer, t.CSizeT(LOG_CAP), Fmt, A, B, C)
LogStr(str(Log_Buffer))
return 1
# 需求 3:扫盘进度行需要 4 个整数(r/dir/file/cand)
def LogFmt4(Fmt: str, A: t.CSizeT, B: t.CSizeT, C: t.CSizeT, D: t.CSizeT) -> int:
if Log_Buffer is None:
return 0
viperlib.snprintf(Log_Buffer, t.CSizeT(LOG_CAP), Fmt, A, B, C, D)
LogStr(str(Log_Buffer))
return 1
def LogFmtS1(Fmt: str, A: str) -> int:
if Log_Buffer is None:
return 0
viperlib.snprintf(Log_Buffer, t.CSizeT(LOG_CAP), Fmt, A)
LogStr(str(Log_Buffer))
return 1
def LogFmtS2(Fmt: str, A: str, B: str) -> int:
if Log_Buffer is None:
return 0
viperlib.snprintf(Log_Buffer, t.CSizeT(LOG_CAP), Fmt, A, B)
LogStr(str(Log_Buffer))
return 1
def LogFmtIS(Fmt: str, A: t.CSizeT, B: str) -> int:
if Log_Buffer is None:
return 0
viperlib.snprintf(Log_Buffer, t.CSizeT(LOG_CAP), Fmt, A, B)
LogStr(str(Log_Buffer))
return 1
# ============================================================
# verbose 开关与"仅 verbose 才输出"的兄弟函数
# * 开关集中在 Verbose_Enabled 一处;调用点只写 Verbose*,不写 if。
# * 关闭时直接返回 0(连格式化都不做),打开时逐字转发给对应 Log*。
# * 返回码与 Log* 一致(1=已写出,0=未写出/失败)。
# ============================================================
Verbose_Enabled: int = 0 # 0=默认安静;1=--verbose
def SetVerbose(V: int):
global Verbose_Enabled
Verbose_Enabled = V
def VerboseOn() -> int:
return Verbose_Enabled
def VerboseStr(S: str) -> int:
if Verbose_Enabled == 0:
return 0
LogStr(S)
return 1
def VerboseFmt1(Fmt: str, A: t.CSizeT) -> int:
if Verbose_Enabled == 0:
return 0
LogFmt1(Fmt, A)
return 1
def VerboseFmt2(Fmt: str, A: t.CSizeT, B: t.CSizeT) -> int:
if Verbose_Enabled == 0:
return 0
LogFmt2(Fmt, A, B)
return 1
def VerboseFmt3(Fmt: str, A: t.CSizeT, B: t.CSizeT, C: t.CSizeT) -> int:
if Verbose_Enabled == 0:
return 0
LogFmt3(Fmt, A, B, C)
return 1
def VerboseFmt4(Fmt: str, A: t.CSizeT, B: t.CSizeT, C: t.CSizeT, D: t.CSizeT) -> int:
if Verbose_Enabled == 0:
return 0
LogFmt4(Fmt, A, B, C, D)
return 1
def VerboseFmtS1(Fmt: str, A: str) -> int:
if Verbose_Enabled == 0:
return 0
LogFmtS1(Fmt, A)
return 1
def VerboseFmtS2(Fmt: str, A: str, B: str) -> int:
if Verbose_Enabled == 0:
return 0
LogFmtS2(Fmt, A, B)
return 1
def VerboseFmtIS(Fmt: str, A: t.CSizeT, B: str) -> int:
if Verbose_Enabled == 0:
return 0
LogFmtIS(Fmt, A, B)
return 1
# stdout 原样写(scan 模式只输出 JSON)
def StdoutWrite(S: str):
if S is None:
return
H: wb.HANDLE = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE)
if H is None or H == wb.INVALID_HANDLE_VALUE:
return
n: t.CSizeT = string.strlen(S)
if n == 0:
return
bw: ULONG = 0
wf.WriteFile(H, S, ULONG(n), t.CPtr(c.Addr(bw)), None)
# ============================================================
# 时间戳(需求 3:轮次开始/下轮开始)
# 用 Win32 本地时间;AddMs 通过 FILETIME(100ns) 加法后回转 SYSTEMTIME。
# 依据:wb.FILETIME 为 {dwLowDateTime,dwHighDateTime}(win32base.vp:36),
# FILETIME 单位 100ns(Win32 规范),故 AddMs*10000 为增量。
# ============================================================
def TimeStampNow(Out: bytes, Cap: int) -> int:
St: wb.SYSTEMTIME = wb.SYSTEMTIME()
wb.GetLocalTime(t.CPtr(c.Addr(St)))
viperlib.snprintf(Out, t.CSizeT(Cap), "%04u-%02u-%02u %02u:%02u:%02u",
t.CUInt32T(St.wYear), t.CUInt32T(St.wMonth), t.CUInt32T(St.wDay),
t.CUInt32T(St.wHour), t.CUInt32T(St.wMinute), t.CUInt32T(St.wSecond))
return 1
def TimeStampAfter(Out: bytes, Cap: int, AddMs: t.CSizeT) -> int:
St: wb.SYSTEMTIME = wb.SYSTEMTIME()
wb.GetLocalTime(t.CPtr(c.Addr(St)))
Ft: wb.FILETIME = wb.FILETIME()
if wb.SystemTimeToFileTime(t.CPtr(c.Addr(St)), t.CPtr(c.Addr(Ft))) == 0:
return TimeStampNow(Out, Cap)
Ticks: ULONGLONG = (ULONGLONG(Ft.dwHighDateTime) << 32) | ULONGLONG(Ft.dwLowDateTime)
Ticks = Ticks + (ULONGLONG(AddMs) * ULONGLONG(10000))
Ft.dwLowDateTime = DWORD(Ticks & ULONGLONG(0xFFFFFFFF))
Ft.dwHighDateTime = DWORD(Ticks >> 32)
St2: wb.SYSTEMTIME = wb.SYSTEMTIME()
if wb.FileTimeToSystemTime(t.CPtr(c.Addr(Ft)), t.CPtr(c.Addr(St2))) == 0:
return TimeStampNow(Out, Cap)
viperlib.snprintf(Out, t.CSizeT(Cap), "%04u-%02u-%02u %02u:%02u:%02u",
t.CUInt32T(St2.wYear), t.CUInt32T(St2.wMonth), t.CUInt32T(St2.wDay),
t.CUInt32T(St2.wHour), t.CUInt32T(St2.wMinute), t.CUInt32T(St2.wSecond))
return 1
+482
View File
@@ -0,0 +1,482 @@
import t, c
from stdint import *
import stdlib
import string
import viperlib
import sys
import lib.VFs as VFs
import lib.VLog as VLog
import lib.VDirs as VDirs
import lib.VProc as VProc
import lib.VCli as VCli
import w32.win32file as wf
import w32.win32base as wb
# ============================================================
# VPe - PE/COFF 解析 + ver 子命令流程
#
# 从原 App/main.vp 平移 PE 解析(ReadU16/ReadU32/节表/.PRVERSN/arch)与 [1/3] 诊断,
# 并承载 ver 的三段式流程([0/3] 目录表 → [1/3] 段读取 → [2/3] --version 前缀)。
#
# 依赖:VFs(路径/读文件/通用字节)、VLog(日志)、VDirs(目录表)、VProc(带超时探测)、VCli(版本号)。
# 偏移/常量依据:PE/COFF 规范;本仓原实现(本次拆分前 App/main.vp:29-58、:517-988)。
#
# 日志详细度:逐次 PE 解析中间量([pe] 行)、被校验目标路径、目录表逐条列举、--version 探测
# 细节属过程细节,走 VLog.Verbose*(默认安静,仅 --verbose 时输出);ver 的 [x/3] 步骤结果、
# PE/版本段失败原因、警告与降级说明始终输出,且不内嵌被处理路径。
# ============================================================
# PE/COFF 常量
DOS_SIGNATURE: t.CDefine = 0x5A4D # "MZ" 小端
NT_SIGNATURE: t.CDefine = 0x00004550 # "PE\0\0" 小端
OPT_MAGIC_PE32: t.CDefine = 0x10B
OPT_MAGIC_PE32P: t.CDefine = 0x20B
SEC_ENTRY_SIZE: t.CDefine = 40
E_LFANEW_OFF: t.CDefine = 0x3C # DOS 头 e_lfanew
COFF_MACHINE_OFF: t.CDefine = 4 # PE签名(4)+COFF: Machine @ COFF+0
COFF_NUMSEC_OFF: t.CDefine = 6 # PE签名(4)+COFF: NumberOfSections @ COFF+2
COFF_OPTSIZE_OFF: t.CDefine = 20 # PE签名(4)+COFF: SizeOfOptionalHeader @ COFF+16
OPT_MAGIC_OFF: t.CDefine = 24 # PE签名(4)+COFF(20) 之后为可选头 Magic
SEC_NAME_OFF: t.CDefine = 0
SEC_RAWSIZE_OFF: t.CDefine = 16
SEC_RAWPTR_OFF: t.CDefine = 20
VERSION_SECTION: t.CDefine = ".PRVERSN"
# COFF Machine 值(winnt.h)→ arch 字符串
MACHINE_X86_64: t.CDefine = 0x8664
MACHINE_X86: t.CDefine = 0x14C
MACHINE_ARM64: t.CDefine = 0xAA64
MACHINE_ARM: t.CDefine = 0x1C0
MACHINE_ARMNT: t.CDefine = 0x1C4
# 扫盘用 PE 读取上限
PE_HEAD_READ: t.CDefine = 8192 # 单次读取的"头部窗口"(DOS+COFF+可选头+节表)
VER_CAP: t.CDefine = 128 # .PRVERSN 版本串缓冲
ARCH_CAP: t.CDefine = 32 # arch 字符串缓冲("unknown(0xFFFF)" 14 字节)
# ParsePeHead 的输出(用模块全局而非数组出参)
Pe_RawPtr: t.CUInt32T = 0
Pe_RawSize: t.CUInt32T = 0
# TryReadPeInfo 打开失败(rc=1)时的 GetLastError(check 通道据此区分 NOT_FOUND / NO_PERMISSION)。
# 生命周期:TryReadPeInfo 每次进入复位为 0;仅在 CreateFileA 返回 INVALID_HANDLE_VALUE 时写入,
# 且**紧跟该调用读取**(其后任何 Win32 调用——包括 PeDbgPrint 里的 WriteFile——都会覆盖它)。
Pe_OpenErr: ULONG = 0
# 失败路径诊断([pe] 行)的中间量:ParsePeHead 每步写入此处,供调用点在失败时打印。
# Pe_DbgGot :本次参与解析的字节数(scan=头部窗口实读字节数;ver=[1/3] 为整文件字节数)
# Pe_DbgSecOff :节表起始偏移 = e_lfanew + 24 + SizeOfOptionalHeader(尚未算出时保持 0)
# Pe_DbgLastRc :最近一次解析失败码(0=成功;scan 路径为 ParsePeHead 的 prc,
# ver 路径为 Phase1VersionSection 按同一条件编码的等价码)
# 生命周期:TryReadPeInfo 每次进入复位;Phase1VersionSection 每次进入复位。
Pe_DbgLfanew: t.CUInt32T = 0
Pe_DbgNsec: t.CUInt32T = 0
Pe_DbgOptSize: t.CUInt32T = 0
Pe_DbgSecOff: t.CUInt32T = 0
Pe_DbgGot: t.CSizeT = 0
Pe_DbgLastRc: int = 0
# ============================================================
# 小端读取(Buf 为无符号字节基址,Buf[i] 即 u8)
# ============================================================
def ReadU16(Buf: BYTEPTR, Off: int) -> t.CUInt32T:
Val: t.CUInt32T = t.CUInt32T(Buf[Off])
Val = Val | (t.CUInt32T(Buf[Off + 1]) << 8)
return Val
def ReadU32(Buf: BYTEPTR, Off: int) -> t.CUInt32T:
Val: t.CUInt32T = t.CUInt32T(Buf[Off])
Val = Val | (t.CUInt32T(Buf[Off + 1]) << 8)
Val = Val | (t.CUInt32T(Buf[Off + 2]) << 16)
Val = Val | (t.CUInt32T(Buf[Off + 3]) << 24)
return Val
# 复制 8 字节节名并补 NUL,写入 Out(调用方保证 >=9 字节)
def CopySectionName(Buf: BYTEPTR, Off: int, Out: bytes):
k: int = 0
while k < 8:
Out[k] = Buf[Off + k]
k += 1
Out[8] = 0
# ============================================================
# PE 解析(轻量,无诊断输出;供 scan 与 ver 共用同一套常量/偏移)
# ============================================================
# COFF Machine → arch 字符串。返回 1=已写入
def FillArch(OutArch: bytes, Cap: int, Machine: int) -> int:
if Cap < 24:
return 0
if Machine == MACHINE_X86_64:
VFs.CopyRaw(OutArch, Cap, "x86_64")
elif Machine == MACHINE_X86:
VFs.CopyRaw(OutArch, Cap, "x86")
elif Machine == MACHINE_ARM64:
VFs.CopyRaw(OutArch, Cap, "arm64")
elif Machine == MACHINE_ARM or Machine == MACHINE_ARMNT:
VFs.CopyRaw(OutArch, Cap, "arm")
else:
viperlib.snprintf(OutArch, t.CSizeT(Cap), "unknown(0x%X)", t.CUInt32T(Machine))
return 1
# [诊断] 打印一行 PE 解析中间量(单行由多次固定元数转发拼出,不引入自定义 varargs)。
# Rc:失败码;Fsz:文件大小(该点尚不可知时传 0);RawPtr/RawSize:.PRVERSN 段位置(尚不可知时传 0)。
# sectab_end = sec_off + nsec * 40,由打印时当场推算(尚未解析出的量保持 0)。
# 属"内部路径细节"(逐次解析中间量),默认安静,仅 --verbose 时输出。
def PeDbgPrint(Rc: int, Fsz: t.CSizeT, RawPtr: t.CUInt32T, RawSize: t.CUInt32T):
global Pe_DbgLastRc
Pe_DbgLastRc = Rc
SecTabEnd: t.CSizeT = t.CSizeT(Pe_DbgSecOff) + t.CSizeT(Pe_DbgNsec) * t.CSizeT(SEC_ENTRY_SIZE)
VLog.VerboseFmt4(" [pe] rc=%u lfanew=%u nsec=%u optsize=%u",
t.CSizeT(Rc), t.CSizeT(Pe_DbgLfanew), t.CSizeT(Pe_DbgNsec), t.CSizeT(Pe_DbgOptSize))
VLog.VerboseFmt4(" sec_off=%u sectab_end=%u got=%u fsz=%u",
t.CSizeT(Pe_DbgSecOff), SecTabEnd, Pe_DbgGot, Fsz)
VLog.VerboseFmt2(" rawptr=%u rawsize=%u\n", t.CSizeT(RawPtr), t.CSizeT(RawSize))
# 取"最近一次 TryReadPeInfo 的 CreateFileA 失败错误码"(rc=1 时才有意义;其余情况为 0)。
# check 通道按此映射:2/3(NOT_FOUND) → NOT_FOUND;5/32 → NO_PERMISSION;其它 → NO_PERMISSION。
def PeOpenErr() -> ULONG:
return Pe_OpenErr
# 纯解析:从已读入的"头部窗口" Buf(Got 字节)解析出 .PRVERSN 的 raw_ptr/raw_size。
# 结果写入模块全局 Pe_RawPtr / Pe_RawSize;同时填 OutArch。
# 返回 0=找到 .PRVERSN;1..7=各类失败(仅需 0/非 0 语义):
# 1=Got<0x40(头部窗口不足) 2=缺少 MZ 签名 3=e_lfanew 越界
# 4=缺少 PE 签名 5=可选头 Magic 未知 6=节表超出头部窗口
# 7=未找到 .PRVERSN
# 每个 return 前把失败码写入 Pe_DbgLastRc,关键中间量写入 Pe_DbgLfanew/Pe_DbgNsec/
# Pe_DbgOptSize/Pe_DbgSecOff/Pe_DbgGot,供调用点在失败时打 [pe] 行。
def ParsePeHead(Buf: bytes, Got: t.CSizeT, OutArch: bytes, ArchCap: int) -> int:
global Pe_RawPtr, Pe_RawSize
global Pe_DbgLfanew, Pe_DbgNsec, Pe_DbgOptSize, Pe_DbgSecOff, Pe_DbgGot, Pe_DbgLastRc
Pe_RawPtr = 0
Pe_RawSize = 0
Pe_DbgLfanew = 0
Pe_DbgNsec = 0
Pe_DbgOptSize = 0
Pe_DbgSecOff = 0
Pe_DbgGot = Got
Pe_DbgLastRc = 0
if Got < 0x40:
Pe_DbgLastRc = 1
return 1
ub: BYTEPTR = BYTEPTR(Buf)
if ReadU16(ub, 0) != DOS_SIGNATURE:
Pe_DbgLastRc = 2
return 2
lfanew: t.CUInt32T = ReadU32(ub, E_LFANEW_OFF)
Pe_DbgLfanew = lfanew
if t.CSizeT(lfanew) + 24 > Got:
Pe_DbgLastRc = 3
return 3
if ReadU32(ub, t.CInt(lfanew)) != NT_SIGNATURE:
Pe_DbgLastRc = 4
return 4
nsec: t.CUInt32T = ReadU16(ub, t.CInt(lfanew) + COFF_NUMSEC_OFF)
optsize: t.CUInt32T = ReadU16(ub, t.CInt(lfanew) + COFF_OPTSIZE_OFF)
Pe_DbgNsec = nsec
Pe_DbgOptSize = optsize
magic: t.CUInt32T = ReadU16(ub, t.CInt(lfanew) + OPT_MAGIC_OFF)
if magic != OPT_MAGIC_PE32 and magic != OPT_MAGIC_PE32P:
Pe_DbgLastRc = 5
return 5
mach: t.CUInt32T = ReadU16(ub, t.CInt(lfanew) + COFF_MACHINE_OFF)
FillArch(OutArch, ArchCap, t.CInt(mach))
sec_off: t.CInt = t.CInt(lfanew) + 24 + t.CInt(optsize)
Pe_DbgSecOff = t.CUInt32T(sec_off)
if t.CSizeT(sec_off) + t.CSizeT(nsec) * t.CSizeT(SEC_ENTRY_SIZE) > Got:
Pe_DbgLastRc = 6
return 6 # 节表超出头部窗口(极端多节 PE,跳过)
i: int = 0
while i < t.CInt(nsec):
entry: t.CInt = sec_off + i * SEC_ENTRY_SIZE
if string.strncmp(str(Buf + entry + SEC_NAME_OFF), VERSION_SECTION, 8) == 0:
Pe_RawPtr = ReadU32(ub, entry + SEC_RAWPTR_OFF)
Pe_RawSize = ReadU32(ub, entry + SEC_RAWSIZE_OFF)
return 0
i += 1
Pe_DbgLastRc = 7
return 7
# 把 Src[Off .. Off+Len) 中的版本串(到第一个 NUL 或 VerCap-1)拷入 OutVer
def CopyVersion(Src: bytes, Off: t.CInt, Len: t.CInt, OutVer: bytes, VerCap: int) -> int:
n: t.CInt = 0
while n < Len and n < VerCap - 1:
if Src[Off + n] == 0:
break
OutVer[n] = Src[Off + n]
n += 1
OutVer[n] = 0
return n
# 扫描用轻量 PE 探测:单次打开,先读头部窗口(PE_HEAD_READ)解析节表,段内容不在窗口内时
# 改走整文件读(不再做 seek/二次定位)。成功返回 0 并填 OutVer/OutArch。
# 返回非 0 = 跳过(非 PE / 无 .PRVERSN / 读取失败):
# 1=CreateFileA 打开失败 2=GetFileSizeEx 取文件大小失败
# 3=文件过短 (fsz < 0x40) 4=头部窗口缓冲分配失败
# 5=ReadFull 读取不足 (got < 0x40) 100+prc=ParsePeHead 失败(prc 含义见 ParsePeHead)
# 6=raw_size==0 或 raw_ptr+raw_size 超出文件大小(注意与 106=prc 6 区分)
# 7=整文件读取失败(VFs.ReadWholeFile 返回 None;原因见 VFs.LastReadErr)
# 每个非 0 返回前先打一行 [pe] 诊断(中间量见 Pe_Dbg*;本次尚未解析出的量打印 0)。
def TryReadPeInfo(Target: str, OutVer: bytes, VerCap: int, OutArch: bytes, ArchCap: int) -> int:
global Pe_DbgLfanew, Pe_DbgNsec, Pe_DbgOptSize, Pe_DbgSecOff, Pe_DbgGot, Pe_DbgLastRc, Pe_OpenErr
OutVer[0] = 0
OutArch[0] = 0
# 每次进入先复位诊断中间量:本次 [pe] 行只反映本次解析(尚未解析出的量保持 0)
Pe_DbgLfanew = 0
Pe_DbgNsec = 0
Pe_DbgOptSize = 0
Pe_DbgSecOff = 0
Pe_DbgGot = 0
Pe_DbgLastRc = 0
Pe_OpenErr = 0
h: wb.HANDLE = wf.CreateFileA(
Target, wf.GENERIC_READ,
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE | wf.FILE_SHARE_DELETE,
None, wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, None)
if h == wb.INVALID_HANDLE_VALUE:
Pe_OpenErr = wb.GetLastError() # 紧跟 CreateFileA 读取(PeDbgPrint 的写日志会覆盖)
PeDbgPrint(1, t.CSizeT(0), t.CUInt32T(0), t.CUInt32T(0))
return 1
li: wb.LARGE_INTEGER = wb.LARGE_INTEGER()
if wf.GetFileSizeEx(h, t.CPtr(c.Addr(li))) == 0:
wb.CloseHandle(h)
PeDbgPrint(2, t.CSizeT(0), t.CUInt32T(0), t.CUInt32T(0))
return 2
fsz: t.CSizeT = t.CSizeT(li.QuadPart)
if fsz < 0x40:
wb.CloseHandle(h)
PeDbgPrint(3, fsz, t.CUInt32T(0), t.CUInt32T(0))
return 3
head_cap: t.CSizeT = t.CSizeT(PE_HEAD_READ)
if fsz < head_cap:
head_cap = fsz
head: bytes = stdlib.malloc(head_cap + 1)
if head is None:
wb.CloseHandle(h)
PeDbgPrint(4, fsz, t.CUInt32T(0), t.CUInt32T(0))
return 4
got: t.CSizeT = VFs.ReadFull(h, head, head_cap)
head[got] = 0
Pe_DbgGot = got
if got < 0x40:
stdlib.free(t.CPtr(head))
wb.CloseHandle(h)
PeDbgPrint(5, fsz, t.CUInt32T(0), t.CUInt32T(0))
return 5
prc: int = ParsePeHead(head, got, OutArch, ArchCap)
if prc != 0:
stdlib.free(t.CPtr(head))
wb.CloseHandle(h)
PeDbgPrint(100 + prc, fsz, Pe_RawPtr, Pe_RawSize)
return 100 + prc
raw_ptr: t.CUInt32T = Pe_RawPtr
raw_size: t.CUInt32T = Pe_RawSize
if raw_size == 0 or t.CSizeT(raw_ptr) + t.CSizeT(raw_size) > fsz:
stdlib.free(t.CPtr(head))
wb.CloseHandle(h)
PeDbgPrint(6, fsz, raw_ptr, raw_size)
return 6
if t.CSizeT(raw_ptr) + t.CSizeT(raw_size) <= got:
# 段内容已在头部窗口内
CopyVersion(head, t.CInt(raw_ptr), t.CInt(raw_size), OutVer, VerCap)
else:
# 段内容不在头部窗口内:改走"整文件读"路径(与 ver [1/3] 同一套 API,已验证可用)。
# 这里不用 wf.SetFilePointerEx 定位:其 liDistanceToMove 在真实 Win32 上按值传 8 字节,
# 传地址时"距离"会变成该地址值,文件指针被推到 EOF 之后,后续 ReadFile 只能读回 0 字节。
# VFs 据此实现的 SeekTo 已删除(零调用点),见 VFs.vp:19-24。
stdlib.free(t.CPtr(head))
wb.CloseHandle(h)
whole: bytes = VFs.ReadWholeFile(
Target, 0,
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE | wf.FILE_SHARE_DELETE)
if whole is None:
PeDbgPrint(7, fsz, raw_ptr, raw_size)
return 7
CopyVersion(whole, t.CInt(raw_ptr), t.CInt(raw_size), OutVer, VerCap)
stdlib.free(t.CPtr(whole))
return 0
stdlib.free(t.CPtr(head))
wb.CloseHandle(h)
return 0
# ============================================================
# ver [1/3]:打开目标 exe,解析 PE 节表,读取 .PRVERSN 段内容(保留原有逐条诊断输出)
# 返回 0=找到并通过,1=失败
# 本段是独立解析流程(不调用 ParsePeHead);失败时打 [pe] 行,其中 rc 按 ParsePeHead 的
# 同一条件编码:1=字节数<0x40 2=缺少 MZ 签名 3=e_lfanew 越界 4=缺少 PE 签名
# 5=可选头 Magic 未知 6=节表越界(段内容越界亦按 6 打印) 7=未找到 .PRVERSN
# ============================================================
def Phase1VersionSection(Target: str) -> int:
global Pe_DbgLfanew, Pe_DbgNsec, Pe_DbgOptSize, Pe_DbgSecOff, Pe_DbgGot, Pe_DbgLastRc
# 每次进入先复位诊断中间量:本次 [pe] 行只反映本次解析(尚未解析出的量保持 0)
Pe_DbgLfanew = 0
Pe_DbgNsec = 0
Pe_DbgOptSize = 0
Pe_DbgSecOff = 0
Pe_DbgGot = 0
Pe_DbgLastRc = 0
buf: bytes = VFs.ReadWholeFile(Target, 0,
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE | wf.FILE_SHARE_DELETE)
if buf is None:
rc_err: int = VFs.LastReadErr
if rc_err == 1:
VLog.LogFmt1(" 打开文件失败 (win32 err=%u)\n", t.CSizeT(VFs.LastReadWinErr))
elif rc_err == 2:
VLog.LogStr(" 读取文件大小失败\n")
elif rc_err == 3:
VLog.LogStr(" 文件为空\n")
else:
VLog.LogStr(" 分配缓冲区失败\n")
return 1
total: t.CSizeT = VFs.LastReadSize
Pe_DbgGot = total
if total < 0x40:
VLog.LogStr(" 不是有效的 PE 文件(文件过短)\n")
PeDbgPrint(1, total, t.CUInt32T(0), t.CUInt32T(0))
stdlib.free(t.CPtr(buf))
return 1
ub: BYTEPTR = BYTEPTR(buf)
# DOS 头
if ReadU16(ub, 0) != DOS_SIGNATURE:
VLog.LogStr(" 不是有效的 PE 文件(缺少 MZ 头)\n")
PeDbgPrint(2, total, t.CUInt32T(0), t.CUInt32T(0))
stdlib.free(t.CPtr(buf))
return 1
# NT 头
lfanew: t.CUInt32T = ReadU32(ub, E_LFANEW_OFF)
Pe_DbgLfanew = lfanew
if t.CSizeT(lfanew) + 24 > total:
VLog.LogStr(" 不是有效的 PE 文件(e_lfanew 越界)\n")
PeDbgPrint(3, total, t.CUInt32T(0), t.CUInt32T(0))
stdlib.free(t.CPtr(buf))
return 1
if ReadU32(ub, t.CInt(lfanew)) != NT_SIGNATURE:
VLog.LogStr(" 不是有效的 PE 文件(缺少 PE 签名)\n")
PeDbgPrint(4, total, t.CUInt32T(0), t.CUInt32T(0))
stdlib.free(t.CPtr(buf))
return 1
nsec: t.CUInt32T = ReadU16(ub, t.CInt(lfanew) + COFF_NUMSEC_OFF)
optsize: t.CUInt32T = ReadU16(ub, t.CInt(lfanew) + COFF_OPTSIZE_OFF)
Pe_DbgNsec = nsec
Pe_DbgOptSize = optsize
magic: t.CUInt32T = ReadU16(ub, t.CInt(lfanew) + OPT_MAGIC_OFF)
if magic != OPT_MAGIC_PE32 and magic != OPT_MAGIC_PE32P:
VLog.LogFmt1(" 可选头 Magic 未知: 0x%X\n", magic)
PeDbgPrint(5, total, t.CUInt32T(0), t.CUInt32T(0))
stdlib.free(t.CPtr(buf))
return 1
sec_off: t.CInt = t.CInt(lfanew) + 24 + t.CInt(optsize)
Pe_DbgSecOff = t.CUInt32T(sec_off)
if t.CSizeT(sec_off) + t.CSizeT(nsec) * SEC_ENTRY_SIZE > total:
VLog.LogFmt1(" 节表越界(节数=%u)\n", nsec)
PeDbgPrint(6, total, t.CUInt32T(0), t.CUInt32T(0))
stdlib.free(t.CPtr(buf))
return 1
# 查找 .PRVERSN
found: int = 0
raw_ptr: t.CUInt32T = 0
raw_size: t.CUInt32T = 0
i: int = 0
while i < t.CInt(nsec):
entry: t.CInt = sec_off + i * SEC_ENTRY_SIZE
if string.strncmp(str(buf + entry + SEC_NAME_OFF), VERSION_SECTION, 8) == 0:
found = 1
raw_size = ReadU32(ub, entry + SEC_RAWSIZE_OFF)
raw_ptr = ReadU32(ub, entry + SEC_RAWPTR_OFF)
break
i += 1
if found == 0:
VLog.LogFmtS1(" 未找到 %s 节\n", VERSION_SECTION)
PeDbgPrint(7, total, t.CUInt32T(0), t.CUInt32T(0))
VLog.LogStr(" 实际节名:\n")
name8: bytes = stdlib.malloc(9)
if name8 is not None:
j: int = 0
while j < t.CInt(nsec):
CopySectionName(ub, sec_off + j * SEC_ENTRY_SIZE, name8)
VLog.LogFmtS1(" %s\n", str(name8))
j += 1
stdlib.free(t.CPtr(name8))
stdlib.free(t.CPtr(buf))
return 1
# 取到第一个 NUL 为止
if t.CSizeT(raw_ptr) + t.CSizeT(raw_size) > total:
VLog.LogFmt2(" 段内容越界 (offset=%u size=%u)\n", raw_ptr, raw_size)
PeDbgPrint(6, total, raw_ptr, raw_size)
stdlib.free(t.CPtr(buf))
return 1
vlen: t.CUInt32T = 0
while vlen < raw_size and ub[t.CInt(raw_ptr) + t.CInt(vlen)] != 0:
vlen += 1
VLog.LogFmt1(" 原始长度: %u 字节\n", vlen)
VLog.LogFmtS1(" 版本串: %s\n", str(buf + t.CInt(raw_ptr)))
stdlib.free(t.CPtr(buf))
return 0
# ============================================================
# ver 子命令:沿用原有 [0/3]→[1/3]→[2/3] 检查与返回码(日志走 stderr)
# ============================================================
def RunVer() -> int:
if sys._argc < 3 or sys._argv[2] is None:
VLog.LogStr("用法: VET.exe ver <目标 exe 路径>\n")
return 1
target: str = sys._argv[2]
# 横幅(不含路径)保留;被校验目标路径属过程细节,仅 --verbose 时输出
VLog.LogFmtS1("VET - Viper Environment Tool %s\n\n", VCli.Version())
VLog.VerboseFmtS1("目标: %s\n\n", target)
VLog.LogStr("[0/3] 常用目录表\n")
rc0: int = VDirs.BuildTable()
if rc0 == 0:
VLog.LogStr(" ✓ 通过\n\n")
else:
VLog.LogStr(" ✗ 失败(表为空)\n\n")
VLog.LogFmtS1("[1/3] 内嵌版本段 (%s)\n", VERSION_SECTION)
rc1: int = Phase1VersionSection(target)
if rc1 == 0:
VLog.LogStr(" ✓ 通过\n\n")
else:
VLog.LogStr(" ✗ 失败\n\n")
VLog.LogStr("[2/3] --version 前缀\n")
rc2: int = VProc.RunVersionPrefixCheck(target)
if rc2 == 0:
VLog.LogStr(" ✓ 通过\n")
else:
VLog.LogStr(" ✗ 失败\n")
if rc0 == 0 and rc1 == 0 and rc2 == 0:
return 0
return 1
+319
View File
@@ -0,0 +1,319 @@
import t, c
from stdint import *
import stdlib
import string
import viperlib
import w32.win32base as wb
import w32.win32file as wf
import w32.win32process as wp
import w32.win32sync as wsync
import lib.VLog as VLog
# ============================================================
# VProc - 带超时的子进程执行(<exe> --version)
#
# 需求 4:不再依赖 includes/subprocess.vp(其 WaitForSingleObject(INFINITE) 无超时,
# 被探测 exe 挂起会卡死整轮)。本模块照搬 subprocess.vp:64 起的
# "CreatePipe + STARTUPINFOA + CreateProcessA + 读管道" 结构,并加超时:
# WaitForSingleObject(hProcess, 5000):
# WAIT_OBJECT_0 → 读干管道取输出;
# WAIT_TIMEOUT → TerminateProcess(hProcess, 1) + WaitForSingleObject(INFINITE) 回收 → 标记超时;
# 任何路径都 CloseHandle(进程/线程/管道读端),写端在父进程 CreateProcess 成功后立即关闭。
#
# 结构体:直接用 w32 声明的类型化结构体(wb.SECURITY_ATTRIBUTES / wp.STARTUPINFOA /
# wp.PROCESS_INFORMATION),字段赋值 + __sizeof__(),与 includes/subprocess.vp:78-123
# 的已验证写法一致;不再用裸字节缓冲 + 偏移写入。
#
# 关键:`bytes` 是指针类型(TransPyC/lib/core/Handles/HandlesBase.py:483 中
# 'bytes': (t.CChar, 1)),故 c.Addr(Buf) 取到的是"指针变量槽"的地址(char**),
# 不是缓冲首地址(char*);缓冲首地址就是 Buf 本身。
#
# 依赖本仓既有声明(未改 includes):
# wf.CreatePipe(includes/w32/win32file.vp:139)、wf.SetHandleInformation(:140)、
# wf.HANDLE_FLAG_INHERIT(:136)、wf.ReadFile(:90)、
# wp.CreateProcessA(includes/w32/win32process.vp:88)、wp.TerminateProcess(:90)、
# wp.GetExitCodeProcess(:91)、wp.CREATE_NO_WINDOW(:23)、wp.STARTF_USESTDHANDLES(:27)、
# wsync.WaitForSingleObject(includes/w32/win32sync.vp:37)、
# wb.CloseHandle(:64)、wb.WAIT_OBJECT_0(:15)、wb.WAIT_TIMEOUT(:16)、wb.INFINITE(:13)。
# ============================================================
VERSION_PROBE_TIMEOUT_MS: t.CDefine = 5000 # --version 探测超时(毫秒)
VERSION_PROBE_DIAG_CAP: t.CDefine = 4096 # ver [2/3] 诊断输出缓冲
PROBE_CMD_EXTRA_CAP: t.CDefine = 32 # 命令缓冲额外量(引号 + " --version" + NUL)
PROBE_TARGET_MAX: t.CDefine = 4096 # 目标路径上界(超限直接判启动失败,不构造命令)
PROBE_LINE_CAP: t.CDefine = 208 # 探测输出"首行"诊断缓冲(200 字符 + NUL + 对齐)
VERSION_PREFIX: t.CDefine = "Viper"
# 结果输出(模块全局);RunVersionProbe 每次进入先复位
Probe_ExitCode: ULONG = 0
Probe_TimedOut: int = 0
# 最近一次"拉起失败"(rc=1)的失败阶段与错误码(check 通道据此产出 `EXE_ERR,0x<HEX>`)。
# Probe_LaunchStage:0=无失败;1=参数/路径非法(未调用任何 Win32);2=命令缓冲分配失败;
# 3=CreatePipe 失败;4=CreateProcessA 失败
# Probe_LaunchErr :失败时的 GetLastError。CreateProcessA 的取值**紧跟该调用读取**
# (其后 stdlib.free 等 Win32 调用会覆盖线程的 last-error)。
# 生命周期:RunVersionProbe 每次进入复位;仅在 rc=1 的出口写入。
Probe_LaunchStage: int = 0
Probe_LaunchErr: ULONG = 0
# 前缀经函数暴露给 VScan(跨模块函数调用路径明确支持;避免跨模块字符串 t.CDefine 取值)
def VersionPrefix() -> str:
return VERSION_PREFIX
# 捕获输出前 5 字符是否为 "Viper"(口径与 ver [2/3] / scan 候选过滤逐字一致:
# string.strncmp(Out, "Viper", 5) == 0;Out 短于 5 字符时 strncmp 在 NUL 处停下 ⇒ 不通过)。
def OutputHasViperPrefix(Out: str) -> int:
if Out is None:
return 0
if string.strncmp(Out, VERSION_PREFIX, 5) == 0:
return 1
return 0
# 取最近一次拉起失败的阶段 / 错误码(rc=1 时才有意义)
def ProbeLaunchStage() -> int:
return Probe_LaunchStage
def ProbeLaunchErr() -> ULONG:
return Probe_LaunchErr
# [诊断] 取捕获输出的首行(到 NUL/CR/LF 或 Cap-1 字符为止),写入 Out(NUL 结尾)。
# 返回写入长度。上界由 Out 中的 NUL(ReadFile 循环末尾所写)保证,不会越读。
def CopyFirstLine(Src: bytes, Out: bytes, Cap: int) -> int:
ub: BYTEPTR = BYTEPTR(Src)
n: int = 0
while n < Cap - 1:
ch: int = int(ub[n])
if ch == 0 or ch == 10 or ch == 13:
break
Out[n] = ub[n]
n += 1
Out[n] = 0
return n
# ============================================================
# 执行 "<exe> --version"(含超时)
# Out/OutCap:捕获缓冲(stdout+stderr 合并;到 EOF 或缓冲满为止,NUL 结尾)
# Trace:1=scan 路径(打印探测前/后各一行);0=ver 路径(不打印这两行)
# 详细度:`[--version] 开始/结束/捕获 N 字节, 首行` 属"逐次探测细节",统一走 VLog.Verbose*
# (默认安静,仅 --verbose 时输出);"警告:" 前缀的失败诊断始终输出。
# 返回:0=进程正常结束(退出码见 Probe_ExitCode)
# 1=启动失败(参数非法/管道/进程创建/缓冲分配失败)
# 2=超时(已 TerminateProcess;输出为部分)
# 失败路径诊断(定位 cand=0 / ver 失败):每条非 0 出口都写明 rc 语义;"警告:" 前缀的
# 失败诊断(含 GetLastError / 等待结果 / 等待时长)始终输出,且不内嵌被探测路径——路径与
# 已捕获内容/首行属逐次探测细节,仅 --verbose 时经 Verbose* 给出(默认不逐路径刷屏)。
# ============================================================
def RunVersionProbe(Target: str, Out: bytes, OutCap: t.CSizeT, Trace: int) -> int:
global Probe_ExitCode, Probe_TimedOut, Probe_LaunchStage, Probe_LaunchErr
Probe_ExitCode = 0
Probe_TimedOut = 0
Probe_LaunchStage = 0
Probe_LaunchErr = 0
if Target is None or Out is None or OutCap < 2:
Probe_LaunchStage = 1
if Trace != 0:
VLog.VerboseStr(" [--version] 启动失败 (rc=1): 参数非法\n")
return 1
if string.strlen(Target) > t.CSizeT(PROBE_TARGET_MAX):
Probe_LaunchStage = 1
VLog.LogStr(" 警告: --version 目标路径过长 (rc=1),已跳过该候选\n")
return 1
T0: ULONGLONG = wb.GetTickCount64()
if Trace != 0:
VLog.VerboseFmtS1(" [--version] 开始: %s\n", Target)
# --- 命令缓冲:"<exe>" --version ---
n: t.CSizeT = string.strlen(Target) + t.CSizeT(PROBE_CMD_EXTRA_CAP)
CmdLine: bytes = stdlib.malloc(n)
if CmdLine is None:
Probe_LaunchStage = 2
VLog.LogStr(" 警告: --version 探测启动失败 (rc=1, 命令缓冲分配失败)\n")
return 1
viperlib.snprintf(CmdLine, n, "\"%s\" --version", Target)
# --- SECURITY_ATTRIBUTES(栈上结构体;必须在 CreatePipe 之前填好) ---
# nLength 必须等于 sizeof(SECURITY_ATTRIBUTES)=24,否则 CreatePipe 直接失败
# (ERROR_INVALID_PARAMETER);lpSecurityDescriptor=NULL 表示默认安全描述符。
Sa: wb.SECURITY_ATTRIBUTES = wb.SECURITY_ATTRIBUTES()
Sa.nLength = t.CUInt32T(wb.SECURITY_ATTRIBUTES.__sizeof__())
Sa.lpSecurityDescriptor = t.CPtr(0)
Sa.bInheritHandle = 1
# --- 管道(可继承;父进程随后关写端) ---
hRead: wb.HANDLE = None
hWrite: wb.HANDLE = None
if wf.CreatePipe(t.CPtr(c.Addr(hRead)), t.CPtr(c.Addr(hWrite)),
t.CPtr(c.Addr(Sa)), 0) == 0:
PipeErr: ULONG = wb.GetLastError() # 紧跟 CreatePipe 读取
Probe_LaunchStage = 3
Probe_LaunchErr = PipeErr
stdlib.free(t.CPtr(CmdLine))
VLog.LogStr(" 警告: --version 探测启动失败 (rc=1, CreatePipe)\n")
return 1
# 读端不继承给子进程(写端继承由 SECURITY_ATTRIBUTES.bInheritHandle 决定)
wf.SetHandleInformation(hRead, wf.HANDLE_FLAG_INHERIT, 0)
# --- STARTUPINFOA:cb=sizeof, dwFlags=STARTF_USESTDHANDLES, hStdOutput=hStdError=写端 ---
Si: wp.STARTUPINFOA = wp.STARTUPINFOA()
Si.cb = t.CUInt32T(wp.STARTUPINFOA.__sizeof__())
Si.dwFlags = wp.STARTF_USESTDHANDLES
Si.hStdOutput = hWrite
Si.hStdError = hWrite
Pi: wp.PROCESS_INFORMATION = wp.PROCESS_INFORMATION()
# --- CreateProcessA(bInheritHandles=TRUE;CREATE_NO_WINDOW:输出走管道,不开控制台窗口) ---
Ok: BOOL = wp.CreateProcessA(t.CPtr(0), CmdLine, t.CPtr(0), t.CPtr(0), 1,
wp.CREATE_NO_WINDOW, t.CPtr(0), t.CPtr(0),
t.CPtr(c.Addr(Si)), t.CPtr(c.Addr(Pi)))
# GetLastError 紧跟调用读取:其后 stdlib.free 等都会覆盖它
CreateErr: ULONG = wb.GetLastError()
stdlib.free(t.CPtr(CmdLine))
if Ok == 0:
Probe_LaunchStage = 4
Probe_LaunchErr = CreateErr
wb.CloseHandle(hRead)
wb.CloseHandle(hWrite)
VLog.LogFmt1(" 警告: --version 探测启动失败 (rc=1, CreateProcessA, GetLastError=%u)\n",
t.CSizeT(CreateErr))
return 1
# 父进程关闭写端(子进程已持有其副本);此后管道只由子进程持有写端
wb.CloseHandle(hWrite)
# --- 超时等待:先等进程退出,再读干管道 ---
TimedOut: int = 0
KillMs: t.CSizeT = 0
Wr: ULONG = wsync.WaitForSingleObject(Pi.hProcess, VERSION_PROBE_TIMEOUT_MS)
if Wr != wb.WAIT_OBJECT_0:
# WAIT_TIMEOUT(或 WAIT_FAILED 等):杀进程并回收,避免悬挂
TimedOut = 1
KillMs = t.CSizeT(wb.GetTickCount64() - T0)
wp.TerminateProcess(Pi.hProcess, 1)
wsync.WaitForSingleObject(Pi.hProcess, wb.INFINITE)
# 读干管道:子进程已退出/被杀 ⇒ 写端全关,ReadFile 到 EOF 返回
total: t.CSizeT = 0
while total + 1 < OutCap:
br: ULONG = 0
room: ULONG = ULONG(OutCap - 1 - total)
if wf.ReadFile(hRead, Out + total, room, t.CPtr(c.Addr(br)), None) == 0:
break
if br == 0:
break
total += t.CSizeT(br)
Out[total] = 0
ExitCode: ULONG = 0
wp.GetExitCodeProcess(Pi.hProcess, t.CPtr(c.Addr(ExitCode)))
Probe_ExitCode = ExitCode
Probe_TimedOut = TimedOut
wb.CloseHandle(Pi.hProcess)
wb.CloseHandle(Pi.hThread)
wb.CloseHandle(hRead)
Elapsed: t.CSizeT = t.CSizeT(wb.GetTickCount64() - T0)
if TimedOut != 0:
# 超时:写明等待结果(WAIT_TIMEOUT=258 / WAIT_FAILED=4294967295 等)与等待时长。
# 该候选路径与"超时前已捕获内容"属逐次探测细节,仅 --verbose 时给出(默认不逐路径刷屏)
VLog.LogFmt3(" 警告: --version 探测超时 (rc=2, wait=%u, 上限 %u ms),已等待 %u ms 后终止子进程\n",
t.CSizeT(Wr), t.CSizeT(VERSION_PROBE_TIMEOUT_MS), KillMs)
if total == 0:
VLog.VerboseStr(" 警告: 超时前已捕获 0 字节(空)\n")
else:
VLog.VerboseFmt1(" 警告: 超时前已捕获 %u 字节,内容:\n", total)
VLog.VerboseStr(str(Out))
VLog.VerboseStr("\n")
if Trace != 0:
VLog.VerboseFmt4(" [--version] 结束: 耗时 %u ms, 退出码 %u, 超时 %u, 输出 %u 字节\n",
Elapsed, t.CSizeT(ExitCode), t.CSizeT(TimedOut), total)
if TimedOut == 0:
# 正常结束(rc=0):捕获字节数与首行属逐次探测细节,仅 --verbose 时输出
# (ver 路径的 [2/3] 结果仍由 RunVersionPrefixCheck 照常给出)。
Line: bytes = stdlib.malloc(PROBE_LINE_CAP)
if Line is None:
VLog.VerboseFmt1(" [--version] 捕获 %u 字节(首行缓冲分配失败)\n", total)
else:
CopyFirstLine(Out, Line, PROBE_LINE_CAP)
if total == 0:
VLog.VerboseStr(" [--version] 捕获 0 字节(空)\n")
else:
VLog.VerboseFmtIS(" [--version] 捕获 %u 字节, 首行: %s\n", total, str(Line))
stdlib.free(t.CPtr(Line))
if TimedOut != 0:
return 2
return 0
# ============================================================
# ver [2/3]:执行 <exe> --version,校验捕获输出前 5 字符是否为 "Viper"
# 返回 0=通过,1=失败(启动失败/超时/前缀不符)
# 行为增强:改用带超时探测(原实现无超时),其余日志与判定口径不变。
# ============================================================
def RunVersionPrefixCheck(Target: str) -> int:
OutCap: t.CSizeT = t.CSizeT(VERSION_PROBE_DIAG_CAP)
Out: bytes = stdlib.malloc(OutCap)
if Out is None:
VLog.LogStr(" 分配缓冲区失败\n")
return 1
rc: int = RunVersionProbe(Target, Out, OutCap, 0)
if rc == 1:
VLog.LogStr(" 子进程启动失败\n")
stdlib.free(t.CPtr(Out))
return 1
if rc == 2:
VLog.LogStr(" 探测超时,判定失败\n")
stdlib.free(t.CPtr(Out))
return 1
VLog.LogFmt1(" 退出码: %d\n", t.CSizeT(t.CUInt32T(Probe_ExitCode)))
out: str = str(Out)
if out is None:
VLog.LogStr(" 未捕获到 stdout\n")
stdlib.free(t.CPtr(Out))
return 1
head: bytes = stdlib.malloc(6)
if head is None:
VLog.LogStr(" 分配缓冲区失败\n")
stdlib.free(t.CPtr(Out))
return 1
k: int = 0
while k < 5 and out[k] != 0:
head[k] = out[k]
k += 1
head[k] = 0
if string.strncmp(out, VERSION_PREFIX, 5) == 0:
VLog.LogFmtS1(" 前 5 字符: %s\n", str(head))
stdlib.free(t.CPtr(head))
stdlib.free(t.CPtr(Out))
return 0
VLog.LogFmtS2(" 前 5 字符: %s(期望为 %s)\n", str(head), VERSION_PREFIX)
VLog.LogStr(" 原始输出:\n")
VLog.LogStr(out)
VLog.LogStr("\n")
stdlib.free(t.CPtr(head))
stdlib.free(t.CPtr(Out))
return 1
# ============================================================
# 结构体布局(x64 / LLP64):由 w32 声明的类型化结构体 + __sizeof__() 决定,
# 本模块不再手工推导偏移。已核验(只读,本机 Python ctypes,未编译任何东西):
# STARTUPINFOA size=104 align=8: cb@0 dwFlags@60 hStdInput@80 hStdOutput@88 hStdError@96
# PROCESS_INFORMATION size=24 align=8: hProcess@0 hThread@8 dwProcessId@16 dwThreadId@20
# SECURITY_ATTRIBUTES size=24 align=8: nLength@0 lpSecurityDescriptor@8 bInheritHandle@16
# 与 win32base.vp:24 / win32process.vp:42,82 的字段声明一致。
# ============================================================
+618
View File
@@ -0,0 +1,618 @@
import t, c
from stdint import *
import string
import viperlib
import memhub
import lib.VFs as VFs
import lib.VLog as VLog
import lib.VPe as VPe
import lib.VGuid as VGuid
import lib.VJson as VJson
import lib.VCache as VCache
import lib.VCheck as VCheck
import lib.VDirs as VDirs
import lib.VProc as VProc
import lib.VGuard as VGuard
import lib.VCli as VCli
import w32.win32file as wf
import w32.win32base as wb
# ============================================================
# VScan - 扫盘(目录树遍历 / 候选处理 / 进度输出 / 一轮驱动)
#
# 从原 App/main.vp 平移 ScanInit/ScanTree/HandleCandidate/ProbeVersion 与节流;
# 节流开关 Pace_Enabled:常驻模式默认启用(1);未显式给出节流参数时,--once 单轮扫描由 RunScan() 置 0,
# 扫描期间一次都不 sleep(计数与进度输出照常,仅跳过 Sleep)。
# 节流参数(--pace <毫秒> / --pace-units <N>,见 VCli):显式给出其一即启用节流(--once 下也生效)——
# * --pace 设 Pace_Sleep_Ms(0 = 两个节流点都不 Sleep);--pace-units 设 Pace_File_Units(1 = 每个文件都停)。
# * 目录侧粒度恒为 PACE_DIR_UNITS=16(--pace-units 只作用文件侧 PaceFile)。
# * 两者均未给出时逐字沿用缺省:常驻启用 + 20ms + 64,--once 关闭。
#
# 日志详细度(本轮):stderr 默认安静——只打启动摘要(工具名+版本+模式+间隔/once/
# 确认开关,不含路径)、心跳确认提示(看门狗侧)、警告/降级聚合与致命错误、轮末一行
# (不含路径)。过程细节(轮次开始、周期性进度行、每个扫描根进入/离开、逐条 [reject]/
# [pe]/[跳过]/[命中]、逐次 --version 探测、目录表逐条列举、缓存载入明细、轮末"下轮开始"
# 时间戳)一律走 VLog.Verbose*,仅在 --verbose 时输出。日志只写 stderr,stdout 仍只输出
# 每轮全量 JSON。
#
# 心跳确认改由独立看门狗子进程承担(见 VGuard.vp),扫盘侧不再参与确认、也不再被确认打断。
# 轮间隔由 --sleep <毫秒> 指定(VCli.SleepMs(),缺省 10000;0 = 不 sleep)。
#
# --search-paths 查询入口 RunSearchPathsQuery():复用 VDirs.BuildTable 建同一份目录表 →
# 逐行打印(序号/路径/是否会实际扫)→ 返回 0。它不扫盘、不看门狗、不碰缓存,故不经过
# ScanInit、不依赖扫盘静态缓冲(建表只用 VDirs 自己的 _mbuddy 分配)。
#
# check 请求通道(本轮新增,见 VCheck.vp / VGuard.PollIpc):
# * 轮询点(**全部非阻塞**):PaceDir/PaceFile(扫盘高频处)、每次发起 `--version` 探测前、
# 每轮开始与结束。轮询到的 check 请求在本进程内**同步**执行完(读 .PRVERSN → `--version`
# → 回执),期间扫盘停在轮询点 ⇒ check 优先级高于树扫描。
# * 只有开了 --need-confirm(看门狗存在)才可能有请求;未开时 PollIpc 立即返回。
#
# 每轮流程(本轮按需求调整顺序):轮开始 → 轮询 check → **重读 cache.json** →
# **对已发现项做完整重校(不合格剔除)** → 遍历目录树(其间持续轮询 check)→
# 存在性校验 → 轮询 check → 输出本轮数组(stdout)→ 写回 cache.json。
#
# 依赖本仓既有:wf.FindFirstFileA(includes/w32/win32file.vp:115)、wf.FindNextFileA(:117)、
# wf.FindClose(:119)、wf.FILE_ATTRIBUTE_DIRECTORY(:23)、wb.Sleep(:77)、wb.GetTickCount64(:80)。
# ============================================================
MAX_SCAN_DEPTH: t.CDefine = 6 # 递归深度上限
PACE_DIR_UNITS: t.CDefine = 16 # 每处理 16 个目录项
PACE_FILE_UNITS: t.CDefine = 64 # 每处理 64 个文件
PACE_SLEEP_MS: t.CDefine = 20 # 上述节流点 Sleep(20)
PROG_INTERVAL_MS: t.CDefine = 1000 # 进度行最短间隔(需求 3)
PROG_STAMP_CAP: t.CDefine = 64 # 时间戳文本缓冲
PROG_ELAPSED_CAP: t.CDefine = 32 # "3.2s" 文本缓冲
REJECT_LOG_MAX: t.CDefine = 20 # [reject] 逐条打印上限(超出后只在轮末打一次汇总)
# WIN32_FIND_DATAA:4+8+8+8+4+4+4+4 = 44 之后为 cFileName(见"API 证据表"推导)
FD_ATTRIBUTES_OFF: t.CDefine = 0
FD_FILENAME_OFF: t.CDefine = 44
FIND_DATA_SIZE: t.CDefine = 320 # sizeof(WIN32_FIND_DATAA):318 对齐到 4 = 320
# winnt.h: FILE_ATTRIBUTE_REPARSE_POINT(符号链接/挂载点等重解析点,递归时跳过以防成环)
FILE_ATTRIBUTE_REPARSE_POINT: t.CDefine = 0x400
# 由 App/main.vp 注入
_mbuddy: rawptr[memhub.MemBuddy]
# 扫盘静态缓冲(进程生命周期内复用;单线程,无并发)
Scr_Ver: bytes = None
Scr_Arch: bytes = None
Scr_Id: bytes = None
Scr_Disp: bytes = None
Probe_Buf: bytes = None
Prog_Stamp: bytes = None
Prog_Elapsed: bytes = None
# 节流开关:1=启用节流 sleep(常驻模式默认),0=禁用(--once 单轮扫描时由 RunScan 置 0)
Pace_Enabled: int = 1
# "常规轮次"应当采用的节流开关值(由 RunScan 一次性算出):Pace_Enabled 是**本轮**的实际值,
# 每轮开头由 RunScanRound 从本值恢复,再叠加 --first-no-pace 的"第 1 轮不节流"。
# 拆成两个变量是为了让"首轮特例"只影响第 1 轮,不会污染第 2 轮起的常规规则。
Pace_Configured: int = 1
# 节流时长与文件粒度(可变):初值即缺省常量,未给 --pace/--pace-units 时始终取这两个初值。
# --pace/--pace-units 显式给出时由 RunScan 覆盖(见其开头),且不再被 --once 关闭。
Pace_Sleep_Ms: int = PACE_SLEEP_MS # --pace <毫秒>;0 = 两个节流点都不 Sleep
Pace_File_Units: int = PACE_FILE_UNITS # --pace-units <N>;文件侧粒度(目录侧仍是 PACE_DIR_UNITS)
# 节流计数
Pace_Dir_Count: int = 0
Pace_File_Count: int = 0
# 进度状态(需求 3)
Prog_Round: int = 0
Prog_Roots: int = 0
Prog_T0: ULONGLONG = 0
Prog_LastTick: ULONGLONG = 0
Prog_Dirs: int = 0
Prog_Files: int = 0
Prog_Cands: int = 0
Prog_Cur: str = None
Round_Added: int = 0
# [reject] 失败路径诊断计数(每轮由 RunScanRound 复位;模块级,单线程扫盘无并发)
Reject_Shown: int = 0 # 本轮已逐条打印的 [reject] 行数(上限 REJECT_LOG_MAX)
Reject_Total: int = 0 # 本轮被 PE/.PRVERSN 过滤掉的候选总数(含已逐条打印的)
# ============================================================
# 扫盘策略小工具
# ============================================================
# 是否跳过该目录名(需求约定的黑名单,大小写无关)
def IsSkippedDir(Name: str) -> int:
if VFs.StrEqNoCase(Name, "WinSxS") != 0:
return 1
if VFs.StrEqNoCase(Name, "$Recycle.Bin") != 0:
return 1
if VFs.StrEqNoCase(Name, "System Volume Information") != 0:
return 1
if VFs.StrEqNoCase(Name, "node_modules") != 0:
return 1
if VFs.StrEqNoCase(Name, ".git") != 0:
return 1
return 0
# 文件名是否以 .exe 结尾(大小写无关)
def HasExeSuffix(Name: str) -> int:
n: t.CSizeT = string.strlen(Name)
if n < 4:
return 0
a: int = int(Name[t.CInt(n) - 4])
b: int = int(Name[t.CInt(n) - 3])
d0: int = int(Name[t.CInt(n) - 2])
e0: int = int(Name[t.CInt(n) - 1])
if a >= 65 and a <= 90:
a += 32
if b >= 65 and b <= 90:
b += 32
if d0 >= 65 and d0 <= 90:
d0 += 32
if e0 >= 65 and e0 <= 90:
e0 += 32
if a == 46 and b == 101 and d0 == 120 and e0 == 101: # ".exe"
return 1
return 0
# ============================================================
# 进度输出(需求 3;全部走 VLog → stderr;默认安静,仅 --verbose 时输出)
# 计数器与节流基线的更新不受开关影响,只有"打印"这一步走 Verbose*。
# ============================================================
# 打印一行进度(含 r/dir/file/cand/cur/elapsed),并刷新时间基线
def ProgPrint():
global Prog_LastTick
Now: ULONGLONG = wb.GetTickCount64()
Prog_LastTick = Now
ElapsedMs: t.CSizeT = t.CSizeT(Now - Prog_T0)
Secs: t.CSizeT = ElapsedMs // 1000
Tenths: t.CSizeT = (ElapsedMs % 1000) // 100
viperlib.snprintf(Prog_Elapsed, t.CSizeT(PROG_ELAPSED_CAP), "%u.%us",
t.CUInt32T(Secs), t.CUInt32T(Tenths))
Cur: str = Prog_Cur
if Cur is None:
Cur = "?"
VLog.VerboseFmt4("[scan] r=%u dir=%u file=%u cand=%u",
t.CSizeT(Prog_Round), t.CSizeT(Prog_Dirs), t.CSizeT(Prog_Files), t.CSizeT(Prog_Cands))
VLog.VerboseFmtS2(" cur=%s elapsed=%s\n", Cur, str(Prog_Elapsed))
# 仅在距上次进度输出 >= PROG_INTERVAL_MS 时打印(保证"无输出超过 ~1s"可见)
def ProgMaybePrint():
Now: ULONGLONG = wb.GetTickCount64()
if Now - Prog_LastTick >= ULONGLONG(PROG_INTERVAL_MS):
ProgPrint()
# 轮次开始:复位计数并打印(轮次 / 扫描根数 / 时间戳)
def ProgReset(RoundIdx: int, Roots: int):
global Prog_Round, Prog_Roots, Prog_T0, Prog_LastTick
global Prog_Dirs, Prog_Files, Prog_Cands, Prog_Cur
Prog_Round = RoundIdx
Prog_Roots = Roots
Prog_T0 = wb.GetTickCount64()
Prog_LastTick = Prog_T0
Prog_Dirs = 0
Prog_Files = 0
Prog_Cands = 0
Prog_Cur = None
VLog.TimeStampNow(Prog_Stamp, PROG_STAMP_CAP)
VLog.VerboseFmt2("==== 扫盘轮次 %u 开始 (roots=%u", t.CSizeT(RoundIdx), t.CSizeT(Roots))
VLog.VerboseFmtS1(", t=%s) ====\n", str(Prog_Stamp))
def ProgRootEnter(Dir: str):
VLog.VerboseFmtS1(" [root] 进入: %s\n", Dir)
def ProgRootLeave(Dir: str, ElapsedMs: t.CSizeT):
global Prog_Cur
VLog.VerboseFmtIS(" [root] 离开 (耗时 %u ms): %s\n", ElapsedMs, Dir)
Prog_Cur = None # 该根结束后其路径缓冲即将释放,避免遗留悬空指针
def PaceDir():
global Pace_Dir_Count, Prog_Dirs
VGuard.PollIpc() # check 通道轮询点(非阻塞;无通道/无数据时立即返回)
Pace_Dir_Count += 1
Prog_Dirs += 1
if Pace_Dir_Count >= PACE_DIR_UNITS: # 目录侧粒度恒为 PACE_DIR_UNITS=16(--pace-units 只作用文件粒度)
Pace_Dir_Count = 0
# Pace_Sleep_Ms == 0(--pace 0):不调用 Sleep(0)——它不是"关闭节流",而是让出时间片,
# 与本参数的"关闭节流 sleep"语义不符。计数器与下方进度输出照常推进。
if Pace_Enabled != 0 and Pace_Sleep_Ms > 0:
wb.Sleep(ULONG(Pace_Sleep_Ms))
ProgPrint()
else:
ProgMaybePrint()
def PaceFile():
global Pace_File_Count, Prog_Files
VGuard.PollIpc() # check 通道轮询点(非阻塞;无通道/无数据时立即返回)
Pace_File_Count += 1
Prog_Files += 1
if Pace_File_Count >= Pace_File_Units: # 文件侧粒度由 --pace-units 决定(缺省 PACE_FILE_UNITS=64)
Pace_File_Count = 0
if Pace_Enabled != 0 and Pace_Sleep_Ms > 0: # 理由同 PaceDir(--pace 0 不 Sleep(0))
wb.Sleep(ULONG(Pace_Sleep_Ms))
ProgMaybePrint()
# ============================================================
# 初始化
# ============================================================
# 分配扫盘静态缓冲(进程生命周期内复用)
def ScanInit() -> int:
global Scr_Ver, Scr_Arch, Scr_Id, Scr_Disp, Probe_Buf, Prog_Stamp, Prog_Elapsed
Scr_Ver = _mbuddy.alloc(VPe.VER_CAP)
Scr_Arch = _mbuddy.alloc(VPe.ARCH_CAP)
Scr_Id = _mbuddy.alloc(VGuid.GUID_STR_CAP)
Scr_Disp = _mbuddy.alloc(VCache.DISP_CAP)
Probe_Buf = _mbuddy.alloc(VProc.VERSION_PROBE_DIAG_CAP)
Prog_Stamp = _mbuddy.alloc(PROG_STAMP_CAP)
Prog_Elapsed = _mbuddy.alloc(PROG_ELAPSED_CAP)
if Scr_Ver is None or Scr_Arch is None or Scr_Id is None or Scr_Disp is None \
or Probe_Buf is None or Prog_Stamp is None or Prog_Elapsed is None:
VLog.LogStr("[VET] FATAL: 扫盘缓冲分配失败\n")
return 0
if VJson.JsonInit() == 0:
VLog.LogStr("[VET] FATAL: 扫盘缓冲分配失败\n")
return 0
# check 通道缓冲/回执池:分配失败只降级(check 只回执失败原因 CHANNEL_UNAVAILABLE、
# 每轮重校跳过不剔除任何条目),不影响扫盘本身
if VCheck.CheckInit() == 0:
VLog.LogStr("警告: check 通道缓冲不可用,check 请求只回执失败原因(CHANNEL_UNAVAILABLE)、每轮不重校已发现项\n")
return 1
# ============================================================
# 候选处理
# ============================================================
# ----version 前缀探测(带 5s 超时;超时/启动失败 ⇒ 判不通过,不入缓存)
def ProbeVersion(Target: str) -> int:
rc: int = VProc.RunVersionProbe(Target, Probe_Buf, t.CSizeT(VProc.VERSION_PROBE_DIAG_CAP), 1)
if rc == 0:
if VProc.OutputHasViperPrefix(str(Probe_Buf)) != 0:
return 1
return 0
# 处理一个 .exe 候选:过滤 PE + .PRVERSN,通过后 `--version` 校验,命中则入缓存。
# 返回 1=新增命中,0=未命中/已存在/失败
#
# rc 含义表(VPe.TryReadPeInfo 返回码;同 rc 的中间量在该函数内以 [pe] 行打印):
# 1 CreateFileA 打开失败 VPe.vp TryReadPeInfo
# 2 GetFileSizeEx 取文件大小失败 VPe.vp TryReadPeInfo
# 3 文件过短(fsz < 0x40) VPe.vp TryReadPeInfo
# 4 头部窗口缓冲分配失败 VPe.vp TryReadPeInfo
# 5 ReadFull 读取不足(got < 0x40) VPe.vp TryReadPeInfo
# 100+prc ParsePeHead 失败:prc 1=Got<0x40 2=非 MZ 3=e_lfanew 越界 4=非 PE 签名
# 5=可选头 magic 未知 6=节表超出头部窗口 7=未找到 .PRVERSN VPe.vp ParsePeHead
# (即 101=Got<0x40、102=非 MZ、103=e_lfanew 越界、104=非 PE 签名、
# 105=magic 未知、106=节表超出头部窗口、107=未找到 .PRVERSN)
# 6 raw_size==0 或 raw_ptr+raw_size 超出文件大小 VPe.vp TryReadPeInfo
# (注意与 106 区分:106 是 ParsePeHead 的 prc=6)
# 7 整文件读取失败(VFs.ReadWholeFile 返回 None) VPe.vp TryReadPeInfo
def HandleCandidate(ExeAnsi: str) -> int:
global Prog_Cands, Round_Added, Reject_Shown, Reject_Total
if VCache.CacheFind(ExeAnsi) >= 0:
return 0 # 已知:不重复调用 --version
rc: int = VPe.TryReadPeInfo(ExeAnsi, Scr_Ver, VPe.VER_CAP, Scr_Arch, VPe.ARCH_CAP)
if rc != 0:
# 非 PE 或无 .PRVERSN:逐条打印前 REJECT_LOG_MAX 条,其余在轮末汇总(仅 --verbose)
Reject_Total += 1
if Reject_Shown < REJECT_LOG_MAX:
Reject_Shown += 1
VLog.VerboseFmtIS(" [reject] rc=%u %s\n", t.CSizeT(rc), ExeAnsi)
return 0
Prog_Cands += 1
# 仅对已过 PE + .PRVERSN 过滤的 exe 调用 --version(这类很少)。
# 发起探测前轮询 check 通道(probe 最长可占 5 秒;把排队的 check 先放行,避免它被一次
# 探测压后——check 优先级高于扫盘)。
VGuard.PollIpc()
if ProbeVersion(ExeAnsi) == 0:
VLog.VerboseFmtS1(" [跳过] PE+PRVERSN 命中但 --version 前缀不符: %s\n", ExeAnsi)
return 0
if VGuid.NewGuidStr(Scr_Id, VGuid.GUID_STR_CAP) == 0:
VLog.LogStr(" 警告: CoCreateGuid 失败,跳过该条目\n")
return 0
viperlib.snprintf(Scr_Disp, t.CSizeT(VCache.DISP_CAP), "Viper %s", Scr_Ver)
if VCache.CacheAdd(ExeAnsi, str(Scr_Id), str(Scr_Ver), str(Scr_Arch),
"workspace", str(Scr_Disp)) == 0:
return 0
Round_Added += 1
VLog.VerboseFmtS2(" [命中] %s (版本 %s)\n", ExeAnsi, str(Scr_Ver))
return 1
# ============================================================
# 目录树遍历
# ============================================================
# 递归枚举目录,查找 .exe。Depth 为当前深度(从 1 起,上限 MAX_SCAN_DEPTH)。
#
# 两遍枚举(减少 FindNextFileA 次数:文件项不再逐个进入本逻辑):
# ① `Dir\*` → 只认目录:递归子目录(跳过重解析点与黑名单目录)
# ② `Dir\*.exe` → 扩展名交给系统过滤,只认文件:逐个交给 HandleCandidate
# 两遍共用同一块模式缓冲与 FIND_DATA 缓冲:先跑完 ① 并 FindClose,再跑 ②;
# 递归帧各自持有自己的缓冲,互不干扰。
# ② 仍保留 HasExeSuffix 守卫:8.3 短名可能让 `*.exe` 命中长名并非 .exe 的文件,
# 后缀判定可把这类误匹配排除掉。
def ScanTree(DirAnsi: str, Depth: int) -> int:
global Prog_Cur
Prog_Cur = DirAnsi # 进度行的"当前目录"
Pat: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
Fd: bytes = _mbuddy.alloc(FIND_DATA_SIZE)
if Pat is None or Fd is None:
if Pat is not None:
_mbuddy.free(t.CPtr(Pat))
if Fd is not None:
_mbuddy.free(t.CPtr(Fd))
return 0
# ---- ① 目录:枚举子目录并递归 ----
if VFs.JoinPath(Pat, VFs.PATH_BUF_CAP, DirAnsi, "*") == 0:
_mbuddy.free(t.CPtr(Pat))
_mbuddy.free(t.CPtr(Fd))
return 0
hDir: wb.HANDLE = wf.FindFirstFileA(Pat, t.CPtr(Fd))
if hDir != wb.INVALID_HANDLE_VALUE:
while True:
PaceDir()
Attrs: t.CUInt32T = VPe.ReadU32(BYTEPTR(Fd), FD_ATTRIBUTES_OFF)
Name: str = str(Fd + FD_FILENAME_OFF)
if string.strcmp(Name, ".") != 0 and string.strcmp(Name, "..") != 0:
if (Attrs & wf.FILE_ATTRIBUTE_DIRECTORY) != 0:
# 跳过重解析点(符号链接/挂载点)与黑名单目录,避免成环与噪声
if (Attrs & FILE_ATTRIBUTE_REPARSE_POINT) == 0 and IsSkippedDir(Name) == 0:
if Depth < MAX_SCAN_DEPTH:
Child: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
if Child is not None:
if VFs.JoinPath(Child, VFs.PATH_BUF_CAP, DirAnsi, Name) != 0:
ScanTree(str(Child), Depth + 1)
Prog_Cur = DirAnsi # 递归返回后复原(Child 即将释放)
_mbuddy.free(t.CPtr(Child))
if wf.FindNextFileA(hDir, t.CPtr(Fd)) == 0:
break
wf.FindClose(hDir)
# ---- ② 文件:只枚举 *.exe(目录名也可能是 *.exe,已在 ① 递归过,此处跳过)----
if VFs.JoinPath(Pat, VFs.PATH_BUF_CAP, DirAnsi, "*.exe") != 0:
hExe: wb.HANDLE = wf.FindFirstFileA(Pat, t.CPtr(Fd))
if hExe != wb.INVALID_HANDLE_VALUE:
while True:
Attrs2: t.CUInt32T = VPe.ReadU32(BYTEPTR(Fd), FD_ATTRIBUTES_OFF)
Name2: str = str(Fd + FD_FILENAME_OFF)
if (Attrs2 & wf.FILE_ATTRIBUTE_DIRECTORY) == 0:
PaceFile()
if HasExeSuffix(Name2) != 0:
Full: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
if Full is not None:
if VFs.JoinPath(Full, VFs.PATH_BUF_CAP, DirAnsi, Name2) != 0:
HandleCandidate(str(Full))
_mbuddy.free(t.CPtr(Full))
if wf.FindNextFileA(hExe, t.CPtr(Fd)) == 0:
break
wf.FindClose(hExe)
_mbuddy.free(t.CPtr(Pat))
_mbuddy.free(t.CPtr(Fd))
return 0
# ============================================================
# 一轮扫盘
# ============================================================
# 对"已发现项"做**完整重校**并剔除不合格者,返回剔除数。
# 口径(需求 7):文件存在 + PE/.PRVERSN 可解析 + `<exe> --version` 前 5 字符为 Viper
# ——即 VCheck.VerifyExecutable(与 check 通道、扫盘候选过滤同一套判定)。
# 顺序:排在普通树扫描之前("优先确认已发现项"),逐条探测前轮询 check 通道
# (每条最多占 5 秒探测窗口,轮询保证排队的 check 先被受理)。
# 代价:每条已发现项都要重新打开/解析/跑一次 --version(这类条目很少,但单条最长 5 秒,
# 见 README 的限制说明)。
def RevalidateDiscovered() -> int:
removed: int = 0
j: int = 0
while j < VCache.CacheCount():
Exe: str = VCache.CacheExe(j) # 先取用(RemoveAt 会释放该槽)
VGuard.PollIpc() # 探测前轮询(check 优先)
if VCheck.VerifyExecutable(Exe) == 0:
VLog.VerboseFmtS1(" [重校剔除] %s\n", Exe)
VCache.RemoveAt(j)
removed += 1
else:
j += 1
return removed
# 执行一轮扫盘:轮开始(轮询 check → 重读 cache.json → 完整重校已发现项)
# → 遍历常用目录 / 递归查 .exe(其间持续轮询 check)→ 校验缓存存在性 → 轮询 check
# → stdout 输出 JSON → 写缓存文件。
def RunScanRound(CachePath: str, RoundIdx: int) -> int:
global Round_Added, Reject_Shown, Reject_Total
global Pace_Enabled
Round_Added = 0
Reject_Shown = 0
Reject_Total = 0
# 本轮节流开关:从"常规值"(RunScan 依据 --pace/--pace-units/--once 算出)恢复,
# 再叠加 --first-no-pace 的"第 1 轮不节流"。放在这里(而不是 RunScan 的循环里)是因为
# 它属于"一轮"的语义:每轮开头都重算一次 ⇒ 第 1 轮的特例不会泄漏到第 2 轮起的常规轮次。
Pace_Enabled = Pace_Configured
if VCli.FirstNoPace() != 0 and RoundIdx == 1:
Pace_Enabled = 0
T0: ULONGLONG = wb.GetTickCount64()
ProgReset(RoundIdx, VDirs.DirCount())
# 轮开始(顺序有讲究):
# ① 重读 cache.json —— 把内存表换成本轮开盘时的文件内容(先清空再重建);
# ② 受理排队的 check 请求 —— 优先级高于扫盘(含重校与树扫描);**放在重读之后**,
# 这样 check 新入库的命中项不会被紧随其后的重读清掉,本轮就能进入数组并写回文件;
# ③ 对已发现项做完整重校并剔除不合格者("优先确认已发现项",排在树扫描之前)。
VCache.ReloadCache(CachePath)
VGuard.PollIpc()
Revalidated: int = RevalidateDiscovered()
Dirs: int = 0
k: int = 0
while k < VDirs.DirCount():
AnsiDir: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
if AnsiDir is not None:
VFs.Utf8ToAnsi(VDirs.DirAt(k), AnsiDir, VFs.PATH_BUF_CAP)
if VFs.PathIsDir(str(AnsiDir)) != 0:
Dirs += 1
R0: ULONGLONG = wb.GetTickCount64()
ProgRootEnter(str(AnsiDir))
ScanTree(str(AnsiDir), 1)
ProgRootLeave(str(AnsiDir), t.CSizeT(wb.GetTickCount64() - R0))
else:
# 自动表里的静态两条(C:\Viper / D:\Viper)与盘符枚举结果可能本就不存在,
# 逐条回显属过程细节,仅在 --verbose 时给出(--path 的显式告警在 VDirs.BuildTable)
VLog.VerboseFmtS1(" 跳过(不存在或非目录): %s\n", str(AnsiDir))
_mbuddy.free(t.CPtr(AnsiDir))
k += 1
# 本轮被 PE/.PRVERSN 过滤掉的候选:前 REJECT_LOG_MAX 条已在 HandleCandidate 逐条打印,
# 这里只补一行汇总(不逐条重打)——逐条与汇总均属过程细节,仅 --verbose 时给出
if Reject_Total > REJECT_LOG_MAX:
VLog.VerboseFmt2(" [reject] 其余省略…(本轮累计 %u 条,已逐条打印前 %u 条)\n",
t.CSizeT(Reject_Total), t.CSizeT(REJECT_LOG_MAX))
# 轮结束前的最后一次轮询:此后到写盘之间不再有等待点(check 命中项因此能进入本轮数组)
VGuard.PollIpc()
removed: int = VCache.ValidateCache()
VJson.EmitJson()
VLog.StdoutWrite(str(VJson.JsonBuf()))
VCache.WriteCacheFile(CachePath, VJson.JsonBuf(), t.CSizeT(VJson.JsonLen()))
T1: ULONGLONG = wb.GetTickCount64()
# 轮末一行(默认保留,不含路径):常驻模式下它是"本轮已跑完"的存活信号(与启动摘要同构),
# --once 时即结束行;"下轮开始"时间戳属下轮过程细节,仅 --verbose。
# "移除"含两部分:完整重校剔除(Revalidated)+ 存在性校验移除(removed)。
VLog.LogFmt3("轮次 %u 结束: 扫描目录 %u 个, 已知条目 %u 项",
t.CSizeT(RoundIdx), t.CSizeT(Dirs), t.CSizeT(VCache.CacheCount()))
VLog.LogFmt3(", 新增 %u 项, 移除 %u 项, 耗时 %u ms",
t.CSizeT(Round_Added), t.CSizeT(Revalidated + removed), t.CSizeT(T1 - T0))
# "下轮开始"时间戳属过程细节(仅 --verbose)
if VLog.VerboseOn() != 0:
VLog.TimeStampAfter(Prog_Stamp, PROG_STAMP_CAP, t.CSizeT(VCli.SleepMs()))
VLog.LogFmtS1(", 下轮开始: %s", str(Prog_Stamp))
VLog.LogStr("\n")
return 0
# ============================================================
# scan 子命令:参数已由 App/main.vp(VCli.ParseScanArgs)解析并完成看门狗拉起;
# 本函数只做初始化 + 常驻循环(轮间隔 --sleep,缺省 10000;0 = 不 sleep)。
# ============================================================
def RunScan() -> int:
global Pace_Enabled, Pace_Configured, Pace_Sleep_Ms, Pace_File_Units
CachePath: str = VCli.CachePath()
Once: int = VCli.Once()
# 启动摘要(默认保留,单行):工具名 + 版本 + 模式 + 参数摘要。
# 不含缓存路径与其他被处理路径(路径属过程细节,仅 --verbose 时经目录表/进度行给出):
# 该行只承担"进程已启动"这一可观测信号,与轮末一行共同构成本模式的存活指示。
VLog.LogFmtS1("VET - Viper Environment Tool %s (scan)", VCli.Version())
VLog.LogFmt2(" sleep=%u once=%u", t.CSizeT(VCli.SleepMs()), t.CSizeT(Once))
VLog.LogFmt1(" need-confirm=%u\n", t.CSizeT(VCli.NeedConfirm()))
if VCli.PaceSet() != 0:
# 显式给出 --pace 或 --pace-units ⇒ 一律启用节流(--once 下也生效),时长/粒度取命令行值;
# 只给其一则另一项保持缺省(--pace 缺省 20ms、--pace-units 缺省 64)——因为 VCli 未给出的那项
# 仍返回其缺省常量。本分支先于下面的 --once 判断,故 --once 不再关闭这类显式节流。
Pace_Enabled = 1
Pace_Configured = 1
Pace_Sleep_Ms = VCli.PaceSleepMs()
Pace_File_Units = VCli.PaceFileUnits()
VLog.VerboseFmt2("--pace: 已启用扫描节流 sleep=%u units=%u\n",
t.CSizeT(Pace_Sleep_Ms), t.CSizeT(Pace_File_Units))
elif Once != 0:
# 未显式给出节流参数时的原有行为:--once 只跑一轮,关闭节流 sleep(PaceDir/PaceFile 均不再 Sleep)
Pace_Enabled = 0
Pace_Configured = 0
VLog.VerboseStr("--once: 已关闭扫描节流 sleep\n")
# --first-no-pace:只改**第 1 轮**的实际开关(每轮开头由 RunScanRound 从 Pace_Configured
# 恢复 + 叠加该特例),故这里不写 Pace_Enabled,只提示一行(属过程细节,仅 --verbose)。
if VCli.FirstNoPace() != 0:
VLog.VerboseStr("--first-no-pace: 第 1 轮不启用节流 sleep\n")
if ScanInit() == 0:
return 1
VLog.VerboseStr("[0/3] 常用目录表\n")
rc0: int = VDirs.BuildTable()
if rc0 == 0:
VLog.VerboseStr(" ✓ 通过\n\n")
else:
# 表为空 = 本轮无扫描根:属功能性异常,默认仍报一行(不再是静默)
VLog.LogStr("警告: 常用目录表为空(本轮无扫描根)\n\n")
# 缓存载入移到每轮开始(RunScanRound → VCache.ReloadCache):需求是"每轮开始重读
# cache.json",故这里不再单独读一次,避免第 1 轮把同一文件解析两遍。
# `--sleep` 之前的第一次读盘由第 1 轮的 ReloadCache 承担(--once 也经过第 1 轮)。
Round: int = 0
while True:
Round += 1
RunScanRound(CachePath, Round)
if Once != 0:
break
# --sleep:轮间隔(缺省 10000;0 = 不 sleep,立即开始下一轮)
SleepMs: int = VCli.SleepMs()
if SleepMs > 0:
wb.Sleep(ULONG(SleepMs))
return 0
# ============================================================
# scan 查询开关 --search-paths:建好"会搜索的目录表"→ 逐行打印(stderr)→ 返回 0
#
# * 表来源与内容全部复用 VDirs.BuildTable(同一份表、同一顺序,含 --path 覆盖语义与
# 逐条告警),本函数不复制任何建表逻辑。
# * 打印口径 = **建表结果**,即 RunScan 每轮按 DirCount()/DirAt() 实际遍历的那份表
# (VScan.vp:440-455):自动表里两条静态目录(C:\Viper / D:\Viper,VDirs.vp:287-288
# 无条件入表、不检查存在性)可能并不存在,RunScanRound 会在遍历时按 VFs.PathIsDir
# 跳过它们。这里用**同一判定**(先 VFs.Utf8ToAnsi 再 VFs.PathIsDir,与
# VScan.vp:443-444 逐字一致)标注"扫盘时会跳过",故标注后与"实际会去搜索的路径"逐条一致。
# * 不扫盘:只对表内条目各做一次属性查询,不做任何目录树遍历(黑名单目录、递归深度、
# 重解析点等只作用于树内部,不影响表内条目)。
# * 退出码恒为 0:查询本身完成即成功(空表时打印明确说明后同样返回 0)。本函数不写 stdout,
# 全部输出走 stderr,故不会污染 scan 的"stdout 只放单行 JSON"约定。
# ============================================================
def RunSearchPathsQuery() -> int:
VDirs.BuildTable()
Src: str = "自动表"
if VDirs.IsOverride() != 0:
Src = "覆盖表 (--path)"
VLog.LogFmtS1("[search] 目录表来源: %s\n", Src)
N: int = VDirs.DirCount()
if N <= 0:
# 空表必须显式说明,避免用户误以为卡住
VLog.LogStr("[search] 没有可搜索的路径(常用目录表为空)\n")
return 0
Ansi: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
if Ansi is None:
VLog.LogStr(" 警告: 查询缓冲分配失败,仅列出路径(不标注是否会被扫盘跳过)\n")
i: int = 0
while i < N:
Path: str = VDirs.DirAt(i)
Skip: int = 0 # 1=扫盘时会跳过;Ansi 不可用时保持 0(不标注)
if Ansi is not None:
VFs.Utf8ToAnsi(Path, Ansi, VFs.PATH_BUF_CAP)
if VFs.PathIsDir(str(Ansi)) == 0:
Skip = 1
if Skip != 0:
VLog.LogFmtIS(" [search] [%u] %s (不存在或非目录,扫盘时会跳过)\n", t.CSizeT(i), Path)
else:
VLog.LogFmtIS(" [search] [%u] %s\n", t.CSizeT(i), Path)
i += 1
if Ansi is not None:
_mbuddy.free(t.CPtr(Ansi))
VLog.LogFmt1(" [search] 合计 %u 条;未扫盘、未读写缓存,查询到此结束\n", t.CSizeT(N))
return 0
+15
View File
@@ -0,0 +1,15 @@
import t, c
from stdint import *
import sys
# ============================================================
# VET App 包入口(包标记文件)
#
# 作用:标记 App/lib 为包,使 App/main.vp 能以 `import lib.X as X` 形式引入子模块
# (与 TransPyV/App/lib/__init__.vp 的包标记约定一致)。
# 共享内存池不在此声明:VET 各子模块自持模块级 `_mbuddy`,由 App/main.vp 统一注入;
# 其它模块只经各模块公开函数访问,不交叉读写本文件的全局。
# ============================================================
LIB_PACKAGE_MARKER: t.CDefine = 1
+115
View File
@@ -0,0 +1,115 @@
import t, c
from stdint import *
import stdlib
import string
import sys
import memhub
import w32.win32console as w32cmd
import lib.VLog as VLog
import lib.VFs as VFs
import lib.VDirs as VDirs
import lib.VGuid as VGuid
import lib.VCache as VCache
import lib.VJson as VJson
import lib.VProc as VProc
import lib.VPe as VPe
import lib.VScan as VScan
import lib.VCheck as VCheck
import lib.VGuard as VGuard
import lib.VCli as VCli
# ============================================================
# VET - Viper Environment Tool(入口)
#
# 三种模式(详见 README):
# ver : 单 exe 校验(目录表 + .PRVERSN 段 + `--version` 前缀)
# scan : 常驻扫盘发现器(轮间隔由 --sleep 指定,缺省 10 秒;stdout 只输出 JSON)
# confirm-watch : 内部隐藏子命令(心跳确认看门狗),由 scan + --need-confirm 自动拉起,
# 不出现在用法里;只做 sleep→等 stdin 的 {"ok": 1},失败即终止父进程。
#
# 本文件只做:控制台代码页 → Log 初始化 → 建主内存池 → 注入各模块 `_mbuddy`
# → 初始化 argv → 分派子命令。所有实现见 lib/ 下各模块。
# ============================================================
CODE_PAGE: t.CDefine = 65001
POOL_SIZE: t.CDefine = 16777216 # MemBuddy 主内存池 16 MiB
@t.CExport
def main() -> int:
w32cmd.SetConsoleOutputCP(CODE_PAGE)
w32cmd.SetConsoleCP(CODE_PAGE)
VLog.LogInit()
# MemBuddy 内存池:sys._init_argv 与各模块的池分配都依赖它
arena: bytes = stdlib.malloc(POOL_SIZE)
if arena is None:
VLog.LogStr("[VET] FATAL: 内存池分配失败\n")
return 1
mb: rawptr[memhub.MemBuddy] = memhub.MemBuddy(arena, POOL_SIZE)
if mb is None:
VLog.LogStr("[VET] FATAL: MemBuddy 初始化失败\n")
return 1
# 逐个注入同一池实例(各模块声明模块级 _mbuddy;不使用 varargs 自定义函数)
# 注意:声明了 _mbuddy 的模块一个都不能漏——漏注入时该模块的 _mbuddy 指向零值静态存储
# (usable == NULL),其全部 alloc 返回 None,功能静默降级(历史现场:VCheck 漏注入 ⇒
# "check 缓冲分配失败")。
VFs._mbuddy = mb
VDirs._mbuddy = mb
VGuid._mbuddy = mb
VCache._mbuddy = mb
VJson._mbuddy = mb
VScan._mbuddy = mb
VCheck._mbuddy = mb
VGuard._mbuddy = mb
sys._mbuddy = mb
sys._init_argv()
if sys._argc < 2 or sys._argv[1] is None:
VCli.PrintUsage()
return 1
Cmd: str = sys._argv[1]
if string.strcmp(Cmd, "ver") == 0:
# ver 不解析开关(目标路径固定在 _argv[2]):argv 中出现 --verbose 即打开细节日志
if VCli.HasVerbose() != 0:
VLog.SetVerbose(1)
return VPe.RunVer()
# `--search-paths` 允许省掉 `scan` 直接写在子命令位置(`VET.exe --search-paths ...`):
# 它是纯查询(不扫盘、不读写缓存),不需要 scan 语境;语义与 `scan --search-paths` 逐字相同。
# 先置位再由 ParseScanArgs 并入(该函数从 argv[2] 起扫,扫不到 argv[1] 上的这个开关)。
if string.strcmp(Cmd, "--search-paths") == 0:
VCli.SetSearchPathsImplicit()
if string.strcmp(Cmd, "scan") == 0 or string.strcmp(Cmd, "--search-paths") == 0:
# 参数在分派层解析:看门狗的"拉起 → 运行 → 回收"跨越整个 RunScan 生命周期,
# 必须在进入 RunScan 之前拿到 --need-confirm/--need-confirm-timeout。
if VCli.ParseScanArgs() != 0:
return 1
# --verbose 生效时机:解析后立即设置,早于"看门狗拉起日志"与"启动摘要",
# 之后所有 VLog.Verbose* 调用据此决定是否输出(默认关闭 = 安静)。
VLog.SetVerbose(VCli.Verbose())
# --search-paths:查询开关——建好"会搜索的目录表"后逐行打印(stderr)并直接退出。
# 插在 SpawnWatchdog 之前:该查询不扫盘、不读写缓存,也就不该因 --need-confirm 把看门狗
# 拉起来(否则会出现"看门狗已在跑、父进程随即退出"的窗口,还得为它做一次无谓的存活校验)。
# 该分支也不经过 VScan.ScanInit,故不分配任何扫盘缓冲——建表只用 VDirs 自己的
# _mbuddy 分配(VDirs.vp:159/169/174/204),注入已在 main.vp:61 完成。
if VCli.SearchPaths() != 0:
return VScan.RunSearchPathsQuery()
# --need-confirm > 0:拉起独立看门狗子进程(与扫盘解耦)。
# 失败即致命:用户显式要求了心跳确认,不能静默降级为"无确认"运行。
if VCli.NeedConfirm() > 0:
if VGuard.SpawnWatchdog(VCli.NeedConfirm(), VCli.NeedConfirmTimeout()) == 0:
VLog.LogStr("错误: 心跳确认看门狗启动失败,已中止 scan\n")
return 1
RcScan: int = VScan.RunScan()
VGuard.StopWatchdog() # 父正常结束(含 --once)务必回收看门狗,杜绝孤儿进程
return RcScan
# 内部隐藏子命令:心跳确认看门狗(不出现在用法里;由上面的 scan 分支以自身 exe 拉起)
if string.strcmp(Cmd, "confirm-watch") == 0:
return VGuard.RunConfirmWatch()
VLog.LogFmtS1("错误: 未知子命令 %s\n", Cmd)
VCli.PrintUsage()
return 1
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 TermiNexus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+639
View File
@@ -0,0 +1,639 @@
# VET — Viper Environment Tool
`VET` 是一个用 [Viper](../wiki/01-overview.md) 语言编写的 **Viper 工具链产物/环境检查器**(定位类似 PET / Python Environment Tool)。
它有两种模式:
- **`ver <exe>`**:对给定的 Windows PE 可执行文件做三项静态/动态校验,确认该产物确实由 TransPyV 工具链生成、且自报版本正确。
- **`scan --cache <path>`**:**常驻扫盘发现器**——默认每 10 秒一轮(可用 `--sleep <毫秒>` 改),遍历"常用目录表",递归查找所有同时满足"可解析 PE 且含 `.PRVERSN` 段且 `<exe> --version` 输出以 `Viper` 开头"的 exe,把结果以 JSON 数组写到 **stdout**,并把发现记录写入 JSON 缓存文件。目录表默认自动构建,也可用可重复的 `--path <dir>` **强制替换**;扫盘过程日志(进度行、扫描根进出、逐条过滤诊断、目录表清单等,含被处理路径)默认**静默**,仅在 `--verbose` 时写到 **stderr**;可用 `--need-confirm <秒>` 开启**心跳确认**——由**独立的看门狗子进程**(`VET.exe confirm-watch …`,内部隐藏命令)每 x 秒要求 stdin 输入一行 `{"ok": 1}`,**期限(默认 5000 ms,可用 `--need-confirm-timeout <毫秒>` 改)到点仍未收到合法 `{"ok": 1}`、或收到形状合法但取值不是 `1` 的行**(如 `{"ok": 0}`)即输出 `{"exit":1, "msg": "timeout/ok!=1"}` 并终止 scan 进程;**形状可疑的行**(去掉前导空白后首字符不是 `{` / 整行不含 `}` / 行长超过 256 字节,**含空行**)被忽略并继续在剩余期限内等待,不判失败。
- **`--need-confirm` 开启时另有一条 `check` 请求通道**(依附心跳,见「check 请求通道」):往 stdin 写一行 `{"check": "<exe 绝对路径>", "id": <整数>}`,看门狗把它转发给 scan 进程,scan **优先**完成"读 `.PRVERSN` → 执行 `--version` 验证",并把结果以**单行 JSON** 回执写到 stdout。该行不参与心跳判定(期限继续走,仍需照常回 `{"ok": 1}`)。
- **`scan --search-paths`**:**查询开关**(无值)——建好"会搜索的目录表"后**逐行打印**(序号 + 路径 + 目录表来源,输出到 **stderr**)并**直接退出**:不扫盘、不拉起看门狗、不读写缓存,因此**未给 `--cache` 也可运行**(见下文「查询开关 `--search-paths`」)。
> **输出流约定**:所有人类可读日志(含 `[0/3]…` 步骤、`[scan]…` 扫盘进度、`[confirm]…` 心跳提示)一律写 **stderr**;**stdout 只输出 JSON 结果**(`ver` 模式不输出 JSON,stdout 为空)。
>
> **日志详细度**:默认(无 `--verbose`)走**安静模式**——stderr 只保留错误/告警与降级聚合、`[confirm]` 心跳提示、以及每轮一行与启动摘要(均不含被处理路径);进度行、扫描根进出、逐条 `[reject]`/`[pe]`/`[跳过]` 诊断、目录表逐条清单、缓存载入明细、`--version` 探测细节等"过程日志"一律静默。加 `--verbose` 恢复上述全部过程日志。
---
## 用法
```powershell
# 单 exe 校验
VET.exe ver <目标 exe 路径>
# 常驻扫盘发现器(每 10 秒一轮)
VET.exe scan --cache <缓存 JSON 路径> [--once] [--sleep <毫秒>] [--pace <毫秒>] [--pace-units <N>] [--first-no-pace] [--need-confirm <秒>] [--verbose] [--path <目录>]...
# 查询将搜索的路径(建表后逐行打印到 stderr 即退出;不扫盘、不读写缓存,--cache 非必填)
VET.exe scan --search-paths
VET.exe scan --search-paths --path D:\X --path D:\Y
# 恢复全部过程日志(进度行/扫描根/逐条过滤诊断/目录表清单/缓存明细/探测细节)
VET.exe scan --cache <缓存 JSON 路径> --once --verbose > result.json
# 心跳确认 + 自定义确认超时(看门狗子进程,每 3 秒要一次 {"ok": 1},超时 2000 ms)
VET.exe scan --cache <缓存 JSON 路径> --need-confirm 3 --need-confirm-timeout 2000
```
- `--cache <path>`:**必填**(缺失即报错退出,返回码 1)。既是启动时的种子来源,也是每轮结束时写回的发现记录文件。**唯一例外**:与 `--search-paths` 同时使用时非必填(该查询只建表 + 打印,不读写缓存);若同时给了 `--cache` 仍按原样校验(只是不使用)。
- `--once`:只跑一轮后退出(便于验证)。
- `--sleep <毫秒>`:**轮间隔**(缺省 `10000`;`0` = 不 sleep 立即开始下一轮;上界 `86400000`)。非法值报错 + 用法 + 返回码 1。
- `--pace <毫秒>`:**节流点睡眠时长**(缺省 `20`;`0` = **关闭节流**,两个节流点都不 Sleep;上界 `60000`)。非法值报错 + 用法 + 返回码 1。见下文「节流参数 `--pace` / `--pace-units`」。
- `--pace-units <N>`:**文件侧节流粒度**——每 `N` 个文件一个节流点(缺省 `64`;`N=1` = **每个文件都停一次**;取值域 `1..1000000`)。`0` 与非法值一律报错 + 用法 + 返回码 1。它**只作用文件侧**,目录侧粒度恒为 `16`。见下文「节流参数 `--pace` / `--pace-units`」。
- `--first-no-pace`(无值):**第 1 轮扫盘不启用节流**,第 2 轮起按常规规则(常驻默认启用;`--pace 0` 则全程不节流)。仅常驻多轮时才有实际差别;不影响节流参数本身。见下文「节流参数 `--pace` / `--pace-units`」。
- `--need-confirm <秒>`:**心跳确认**(缺省/`0` = 关闭;**仅 scan 模式生效**,`ver` 不接受该参数)。见下文「心跳确认」。
- `--need-confirm-timeout <毫秒>`:心跳确认的**等待超时**(缺省 `5000`,上界 `86400000`)。**仅在同时给出 `--need-confirm`(且 >0)时有意义**;单独给出不报错,但会在 stderr 打一行"已忽略"的告警(理由见「心跳确认」)。
- `--verbose`:**输出详细过程日志**(无值开关,缺省关闭 = 安静模式)。开启后恢复:`==== 扫盘轮次 r 开始 ====`、周期性 `[scan] r=… dir=… file=… cand=… cur=<当前目录> elapsed=…`、每个扫描根 `[root] 进入/离开`、逐条 `[reject]`/`[pe]`/`[跳过]`/`[命中]`、目录表逐条清单与来源统计、缓存载入明细、`--version` 探测前后与首行、轮末"下轮开始"时间戳,以及 `ver` 的目标路径与 PE 解析中间量。`ver` 子命令同样接受该开关(`argv` 中出现即生效)。它只改变 stderr 的详细度,不影响 stdout 的 JSON、缓存文件内容与所有计数/节流行为。
- `--path <dir>`:**可重复**。**一旦出现 `--path`,常用目录表被"强制替换"**——完全不做自动构建(不枚举盘符、不加静态 `C:\Viper`/`D:\Viper`、不取桌面),扫描根 = 命令行给出的路径序列(保持给定顺序,按 `strcmp` 去重)。**不存在的 `--path`:stderr 告警并跳过该条,其余继续**(不静默、也不整体退出)。未给 `--path` 时保持原有自动建表行为。
- `--search-paths`:**查询开关**(无值,仅 `scan`)。建好"会搜索的目录表"后**逐行打印**(序号 + 路径 + 目录表来源,全部输出到 **stderr**)并**直接退出**:不扫盘、不拉起看门狗、不读写缓存,`--cache` 因此非必填(退出码恒为 0,空表也返回 0 并打印"没有可搜索的路径")。可与 `--path` / `--verbose` 组合(`--path` 决定表内容,`--verbose` 额外打印建表过程细节)。细节见下文「查询开关 `--search-paths`」。
- 未知参数 / 未知子命令 / 参数缺失:打印用法到 stderr,返回码 1。`--need-confirm` 的取值必须是十进制非负整数字面量(仅 `0`-`9`,上界 `86400`);`--sleep` / `--need-confirm-timeout` / `--pace` 同理(`0`-`9`,上界分别为 `86400000` / `86400000` / `60000`);`--pace-units` 只接受十进制整数字面量 `1`-`1000000`(`0` 与非法值一律报错)。缺值或非法值:报错 + 用法 + 返回码 1。
- `confirm-watch` 是**内部隐藏子命令**(看门狗),不出现在用法里,手工调用无意义(缺少 `--parent-pid` 会直接报错退出)。它的参数由 scan 自动拼出:`--interval <秒> --timeout <毫秒> --parent-pid <pid> --ipc-write <句柄数值>`(最后一个是 check 通道的管道写端句柄,缺省 0 = 无通道)。
### 新参数默认值表
| 参数 | 缺省值 | 上界 | `0` 的含义 | 适用范围 |
|------|--------|------|-----------|----------|
| `--sleep <毫秒>` | `10000` | `86400000`(1 天) | 不 sleep(轮间隔 0) | scan |
| `--pace <毫秒>` | `20` | `60000`(1 分钟) | **关闭节流**(两个节流点都不 Sleep) | scan |
| `--pace-units <N>` | `64` | `1000000` | **非法**(`N` 必须 ≥1,报错退出) | scan |
| `--first-no-pace`(无值) | 关 | — | — | scan(无值开关;第 1 轮不节流,之后按常规规则) |
| `--need-confirm <秒>` | `0`(关闭) | `86400`(1 天) | 关闭心跳确认 | scan |
| `--need-confirm-timeout <毫秒>` | `5000` | `86400000`(1 天) | 不等待(几乎必然判失败) | scan + `--need-confirm>0` |
> `--verbose` 与 `--search-paths` 均为无值开关,缺省关闭,不属于上表的数值参数;`scan` 与 `ver` 均接受 `--verbose`,`--search-paths` 仅 `scan`。
### 节流参数 `--pace` / `--pace-units`
扫盘在高频位置(每个目录项、每个文件)设"节流点"以降低对机器的持续压力:到达节流点时 `Sleep` 一小段。`--pace` 与 `--pace-units` 把原先写死的两个量**开放为命令行旋钮**:
| 旋钮 | 作用 | 缺省 | 取值域 | 特殊取值 |
|------|------|------|--------|----------|
| `--pace <毫秒>` | 节流点的 `Sleep` 时长 | `20` | `0..60000` | `0` = **关闭节流**:两个节流点都不再 `Sleep`(含目录侧) |
| `--pace-units <N>` | **每 N 个文件**一个节流点(文件侧粒度) | `64` | `1..1000000` | `1` = **每个文件都停一次**(最慢) |
- **只作用文件粒度**:`--pace-units` 只改文件侧粒度;目录侧粒度**恒为** `16`(`VScan.PACE_DIR_UNITS`),不受该开关影响。
- **显式给出即启用**:只要命令行出现 `--pace` 或 `--pace-units` 之一,节流即启用——**`--once` 下也生效**(未给出时 `--once` 仍按原样关闭节流)。只给出其一时,另一项保持其缺省(`--pace` 缺省 20 ms、`--pace-units` 缺省 64)。
- **`--pace 0` 的目录侧行为**:`--pace 0` 是"关闭节流",因此目录侧节流点也不再 `Sleep`(不只是文件侧);两个计数器与进度输出逻辑保持不变(`--pace 0` 不调用 `Sleep(0)`——`Sleep(0)` 只是让出时间片,语义上不是"关闭节流")。
- **`--first-no-pace`(无值)**:只让**第 1 轮**不节流,第 2 轮起回到常规规则(常驻默认启用;若同时给了 `--pace 0`,则全程都不节流,该开关无额外效果)。实现上,`RunScan` 先把"常规值"算进 `VScan.Pace_Configured`,每轮开头由 `RunScanRound` 恢复成本轮实际值、再叠加"第 1 轮 ⇒ 0" ⇒ **首轮特例不会泄漏到第 2 轮起**。它不改变 `--pace`/`--pace-units` 的取值,也不改变 `--once`(单轮时第 1 轮即全部,二者叠加结果就是"不节流")。
- **未给出时逐字兼容**:不给这两个开关时,行为与改造前完全一致——常驻默认启用、时长 20 ms、文件粒度 64、目录粒度 16,`--once` 关闭节流。
- 计数与进度不受影响:`--pace` / `--pace-units` 只决定"是否 Sleep / 睡多久 / 每多少个文件睡一次",不改变 `Prog_Dirs`/`Prog_Files` 计数与进度行输出规则。
### 构建(本机自编译,本仓库不预置产物)
```powershell
# 确保 PATH 含 mingw 与 LLVM
$env:PATH = "D:\msys64\mingw64\bin;D:\LLVM\bin;$env:PATH"
# 用 Viper 自举的前端 TransPyV(识别 project.vpj 的 build_dir)
D:\Users\TermiNexus\Desktop\TransPyC\exe\TransPyV.exe `
--project .\project.vpj
```
产物位于 `VET\.tpv_build\output\VET.exe`(`build_dir` = `./.tpv_build`,`linker.output` = `VET.exe`)。
> 本次把单文件 `App/main.vp` 拆成 `App/main.vp` + `App/lib/*.vp`(新增 11 个源文件)。
> 仓库内 `.tpv_build/temp` 可能残留旧单文件构建的中间产物;首次重建建议清缓存:
> `TransPyV.exe --project .\project.vpj --clear-cache`(或直接删除 `VET\.tpv_build`)。
> 多文件项目由 `source_dir`(`./App`)**递归**收集 `.vp/.py`,子目录 `lib/` 会自动纳入。
> 也可用 Python 前端:`python ..\TransPyC\Projectrans.py --project .\project.vpj`,
> 但 `Projectrans.py` **忽略 `build_dir`**(见 `TransPyC/lib/Projectrans/Config.py:98-100`),
> 此时产物落在 `VET\output\VET.exe`。
### 运行示例
```powershell
# 单 exe 校验(日志在 stderr)
.\.tpv_build\output\VET.exe ver ..\exe\TransPyV.exe
# 扫盘:只跑一轮验证;stdout 重定向到文件即得到 JSON
.\.tpv_build\output\VET.exe scan --cache .\vet_cache.json --once > result.json
# 扫盘:仅扫两个指定目录(强制替换目录表;不做任何自动建表)
.\.tpv_build\output\VET.exe scan --cache .\vet_cache.json --once --path .\App --path D:\Viper > result.json
# 查询将搜索的路径(建表后逐行打印到 stderr 即退出;不需要 --cache,也不扫盘)
.\.tpv_build\output\VET.exe scan --search-paths 2>&1 | Tee-Object .\search.log
# 查询覆盖表:只看这两个目录(不存在的会告警并跳过)
.\.tpv_build\output\VET.exe scan --search-paths --path D:\X --path D:\Y
# 常驻扫盘(每 10 秒一轮,Ctrl+C 退出)
.\.tpv_build\output\VET.exe scan --cache .\vet_cache.json
# 扫盘:轮间隔改为 1 秒(--sleep 1000)
.\.tpv_build\output\VET.exe scan --cache .\vet_cache.json --sleep 1000
# 心跳确认:看门狗子进程每 3 秒要求 stdin 输入一行 {"ok": 1},确认超时 2000 ms。
# 注意:确认由独立子进程按固定节拍驱动,与扫盘是否进行无关(scan 必须活得比 interval 长才看得到效果)
.\.tpv_build\output\VET.exe scan --cache .\vet_cache.json --need-confirm 3 --need-confirm-timeout 2000 2>&1 | Tee-Object .\confirm.log
```
`ver` 模式输出形如(**全部在 stderr**;安静模式不含被校验目标路径与目录表清单):
```
VET - Viper Environment Tool 0.2.0
[0/3] 常用目录表
✓ 通过
[1/3] 内嵌版本段 (.PRVERSN)
原始长度: 13 字节
版本串: 0.0.3-dev.428
✓ 通过
[2/3] --version 前缀
退出码: 0
前 5 字符: Viper
✓ 通过
```
加 `--verbose` 后,上述步骤前会先给出目标路径,`[0/3]` 内会列出目录表逐条与来源统计,`[1/3]`/`[2/3]` 内会给出 PE 解析中间量(`[pe]`)与 `--version` 探测细节:
```
VET - Viper Environment Tool 0.2.0
目标: ..\exe\TransPyV.exe
[0/3] 常用目录表
目录表来源: 自动表 (N 条)
[0] C:\Viper
[1] D:\Viper
...
合计 N 个常用目录
✓ 通过
[1/3] 内嵌版本段 (.PRVERSN)
原始长度: 13 字节
版本串: 0.0.3-dev.428
✓ 通过
[2/3] --version 前缀
退出码: 0
前 5 字符: Viper
✓ 通过
```
退出码:`0` = **三项全部通过**(`[0/3]` 仅在"目录表为空"时计为失败);`1` = 任一项失败或参数缺失。
`scan` 模式的 stdout 形如(**单行**;数组与其中每个对象都在同一行,末尾一个换行):
```json
[{"id":"3f2504e0-4f89-11d3-9a0c-0305e82c3301","executable":"C:\\Viper\\VET.exe","version":"0.0.3-dev.428","arch":"x86_64","source":"workspace","displayName":"Viper 0.0.3-dev.428"}]
```
空缓存(本轮无命中)时输出 `[]` + 换行。写入 `--cache` 文件的**内容与 stdout 逐字节相同**
(同一份 `VJson.Json_Buf`,长度 `VJson.JsonLen()`)。
> 序列化/解析均改用仓库自带 `includes/json/**` 库(Viper 源码,**无新增 DLL 依赖**):
> 条目由 `json.object` / `json.object_set` / `json.string_val` 构造后交 `json.write` 输出
> (紧凑单行、转义与 UTF-8 透传由库保证、键序 = 插入序);缓存读回走 `json.parse`。
---
## `ver` 模式检查项
| 检查项 | 说明 |
|--------|------|
| `[0/3] 常用目录表` | 构建常用目录清单(静态两项 + 各存储盘的 `Program Files`/`Program Files (x86)` + 当前用户桌面),非空即视为通过 |
| `[1/3] 内嵌版本段` | 解析目标 exe 的 PE 段表,定位 `.PRVERSN` 段,读出其中的版本串(如 `0.0.3-dev.428`),打印原始长度与字符串 |
| `[2/3] --version 前缀` | 执行 `<目标 exe> --version`,捕获其 stdout,校验前 5 个字符是否为 `Viper` |
### `[0/3] 常用目录表` 细则
- **表来源**:未给 `--path` → **自动表**;给过 `--path` → **覆盖表**。该统计(`目录表来源: 自动表 (N 条)` / `目录表来源: 覆盖表 (N 条)`)与逐条清单一样属过程细节,仅 `--verbose` 时打印。
- **自动表**:
- **静态两条无条件入表**:`C:\Viper`、`D:\Viper` 走 `AddCommonDirForce`,只做 `strcmp` 去重与容量检查,**不检查目录是否存在**。
- **盘符范围**:枚举 `GetLogicalDrives()` 列出的盘符,**只跳过 `DRIVE_NO_ROOT_DIR(1)` 与 `DRIVE_CDROM(5)`**;`DRIVE_REMOVABLE(2)` / `DRIVE_FIXED(3)` / `DRIVE_REMOTE(4)` 都继续探测其 `Program Files` 与 `Program Files (x86)`。
- **`SetErrorMode(SEM_FAILCRITICALERRORS)`**:枚举前调用一次,以抑制"请插入磁盘"等临界错误对话框,并让无介质驱动器(空读卡器/光驱)立即返回错误、避免长时间阻塞。这是**进程级**设置,会**影响本进程后续所有文件操作**且进程生命周期内不自动恢复。
- **桌面取值**:优先 `SHGetKnownFolderPath(FOLDERID_Desktop)`(能正确处理 OneDrive 等重定向),用 `WideCharToMultiByte(CP_UTF8, ...)` 转为 UTF-8 后写入目录表,并 `CoTaskMemFree` 释放返回的 `PWSTR`;失败(`HRESULT < 0` / 转为空或超长 / 转换失败)时**显式退回** `%USERPROFILE%\Desktop`(退回原因照常告警);实际选用的兜底路径属过程细节,仅 `--verbose` 时打印;若桌面路径不可用,另打一行不含路径的告警。
- **覆盖表(`--path`)**:**完全不调用**上面的自动逻辑;表 = 命令行给出的 `--path` 序列(保序,`strcmp` 去重)。逐条 `GetFileAttributesA` 校验存在性,**不存在则告警并跳过该条**,其余继续。表为空时 `[0/3]` 计为失败(与旧行为一致)。
- `ver` 子命令不支持 `--path`,始终使用自动表(保持原行为)。
`.PRVERSN` 段由 TransPyV 源码中形如下面的声明生成:
```python
V_TPV_VERSION: t.CArray[char] | t.attr.section(".PRVERSN") = "0.0.3-dev.428"
```
(见 `TransPyV/App/main.vp:45`,`t.attr.section` 即 `__attribute__((section(...)))`,定义于 `TransPyC/lib/includes/t.py:832`。)
---
## `scan` 模式细则
### 查询开关 `--search-paths`(建表后打印即退出)
`VET.exe scan --search-paths [--path <dir>]... [--verbose]` 是 `scan` 的**查询开关**(无值):它只回答"这一次会去搜索哪些路径",**打印完就直接退出**,完全不进入扫盘循环。
**两种写法等价**:因为它是纯查询(不扫盘、不读写缓存),也可以**省掉子命令**直接写 `VET.exe --search-paths [--path <dir>]... [--verbose]`——子命令位置上就是该开关,分派层把它当作 `scan --search-paths` 处理(`main.vp` 先置位,`ParseScanArgs` 再并入,故"未给 `--cache` 也放行"这条同样生效)。
- **建表**:复用 `VDirs.BuildTable()`(与 `[0/3]`、每轮扫盘**同一份表、同一顺序**,不复制建表逻辑)。给了 `--path` 走覆盖表(完全不做自动构建),否则走自动表(静态两条 + 各盘 `Program Files`/`Program Files (x86)` + 当前用户桌面)。
- **打印**:全部输出到 **stderr**(stdout 保持为空,不违反"stdout 只放单行 JSON"的约定),形如
```
[search] 目录表来源: 自动表
[search] [0] C:\Viper (不存在或非目录,扫盘时会跳过)
[search] [1] D:\Viper (不存在或非目录,扫盘时会跳过)
[search] [2] C:\Program Files
...
[search] 合计 N 条;未扫盘、未读写缓存,查询到此结束
```
序号与顺序 = 目录表内的槽位与顺序(`VDirs.DirCount()`/`VDirs.DirAt(i)`)。**来源**只在表级给出(`自动表` / `覆盖表 (--path)`,取自 `VDirs.IsOverride()`)——`VDirs` 的表内条目是裸路径串,**不保存逐条的来源标记**(静态/盘符枚举/桌面),故逐条来源无从给出,不做重新推导以免与建表逻辑重复。
- **口径 = 建表结果 + 逐条"是否会实际扫"标注**:自动表里的两条静态项(`C:\Viper`/`D:\Viper`)**无条件入表、不检查存在性**,而 `RunScanRound` 遍历时会按 `GetFileAttributesA` 跳过不存在的项。为了让输出与"实际会搜索的路径"逐条一致,本查询对表内条目**用同一判定**(`VFs.Utf8ToAnsi` → `VFs.PathIsDir`,与扫盘遍历处逐字一致)做一次属性查询,对不存在的条目标注 `(不存在或非目录,扫盘时会跳过)`;不存在的条目**照旧打印**(不隐藏),只是标注出来。覆盖表模式下 `--path` 不存在项已在建表时告警并跳过,因此通常不会出现该标注。
- **不做的事**:**不扫盘**(无目录树遍历、无 `--version` 探测、无 `WinSxS`/`.git` 等黑名单与递归深度逻辑——那些只作用于树内部,不影响表内条目);**不拉起看门狗**(即使同时给了 `--need-confirm`,也在 `SpawnWatchdog` **之前**就返回,不存在"看门狗已起、父进程已退"的窗口,也不做那 400 ms 存活校验);**不读写缓存**(`--cache` 非必填,给了也不使用);**不分配扫盘静态缓冲**(不经过 `VScan.ScanInit`,建表只用 `VDirs` 自己的池分配)。
- **退出码**:恒为 `0`(查询本身完成即成功;**空表也返回 0**,此时 stderr 打 `[search] 没有可搜索的路径(常用目录表为空)`,避免用户误以为卡住)。参数错误仍为 `1`(沿用既有校验,`--search-paths` 只放宽 `--cache` 的必填)。
- `ver` 子命令不接受 `--search-paths`;而 `--search-paths` 既可写成 `scan --search-paths`,也可**省掉子命令**直接写在子命令位置(`VET.exe --search-paths ...`,语义逐字相同,见上文「两种写法等价」)。
### 一轮流程
1. **重读 `cache.json`**(`VCache.ReloadCache`:清空内存表 → 按文件重建)。文件不存在/读取失败/解析失败一律按空缓存继续(与启动时同一口径,打告警)。
2. **受理排队的 `check` 请求**(若有;见「check 请求通道」)。放在重读**之后**:这样 check 新入库的命中项不会被紧随其后的重读清掉,本轮就能进入数组并写回文件。
3. **对每条已发现项做完整重校**(顺序在普通树扫描**之前**,即"优先确认已发现项"):**文件存在 + PE/`.PRVERSN` 可解析 + `<exe> --version` 前 5 字符为 `Viper`**;不合格的**从缓存剔除**(同时从本轮结果中消失),逐条明细在 `--verbose` 下打 ` [重校剔除] <路径>`。**任何一轮的重校失败都不影响 cache 写回**:被剔除项已不在内存表中,轮末照常把剩下的条目写回文件。
4. 遍历**常用目录表**(自动表或 `--path` 覆盖表),逐个 `GetFileAttributesA` **确认目录存在**(不存在则跳过并记日志)。
5. 对存在的目录**递归查找 `.exe`**(`FindFirstFileA` / `FindNextFileA` / `FindClose`)。
6. 候选过滤:**PE 可解析** 且 **`.PRVERSN` 段可识别**。
7. 通过过滤后**才**调用 `<exe> --version`(**带 5 秒超时**),要求捕获输出前 5 个字符是 `Viper`。
8. 命中则构造 JSON 条目写入内存缓存;扫描结束后校验缓存项对应的 exe 是否仍存在(不存在则移除);**轮结束前再轮询一次 `check` 请求**。
9. 把**当前完整数组**打到 stdout;把同一份 JSON 写入 `--cache` 指定文件。
10. 若未指定 `--once`,`Sleep(--sleep)`(缺省 10000 ms)后开始下一轮。
> 第 2 步之外,`check` 请求还会在扫盘过程中被**持续受理**(每次枚举一个目录项/一个候选文件、每次发起 `--version` 探测之前、每轮结束前一次);这些都是**非阻塞**轮询点(`PeekNamedPipe` 无数据即返回),因此不会因为"等请求"而拖慢扫盘。
### 存活输出与过程日志(stderr)
**安静模式(默认)**:stderr 只保留以下内容,且均**不含被处理路径**:
- **启动摘要**(单行):`VET - Viper Environment Tool <版本> (scan) sleep=<ms> once=<0|1> need-confirm=<秒>`;
- **每轮一行**:`轮次 <r> 结束: 扫描目录 <n> 个, 已知条目 <m> 项, 新增 <a> 项, 移除 <d> 项, 耗时 <ms> ms`;
- **错误/告警与降级聚合**:参数错误与用法、`FATAL`、`--need-confirm` 看门狗拉起失败(致命),以及确实发生降级时的一行短告警(缓存解析失败按空缓存启动、`--version` 探测超时、JSON 条目因过长/工作池不足被跳过「本轮 N 条」等)。
判断卡死:**启动摘要**给出"进程已启动"的信号,之后每完成一轮打一行**轮末汇总**——长期看不到新的轮末汇总(超过一轮的正常耗时)即为可疑卡死;开启 `--need-confirm` 时,`[confirm]` 心跳行每 x 秒一次,提供一轮之内的独立活跃信号。
**`--verbose`(恢复全部过程日志)**:以下内容仅在开启时输出(含路径的过程细节都在这里):
- **每轮开始**:`==== 扫盘轮次 <r> 开始 (roots=<n>, t=<YYYY-MM-DD HH:MM:SS>) ====`
- **进入/离开每个扫描根**各一行:` [root] 进入: <dir>` / ` [root] 离开 (耗时 <ms> ms): <dir>`
- **周期性进度**(每 16 个目录项 或 距上次输出满 1 秒):形如
`[scan] r=3 dir=1240 file=5678 cand=2 cur=D:\Viper\App\lib elapsed=3.2s`
(`cur` 为当前正在枚举的目录;`cand` 为已通过 `PE+.PRVERSN` 过滤的候选数)
- **逐条候选诊断**:` [reject] rc=<码> <exe>`(每轮逐条上限 20 条,其余在轮末汇总一行)、` [pe] rc=… …`(PE 解析中间量)、` [跳过] …`、` [命中] …`
- **`--version` 探测前后**各一行(仅 scan;含该 exe 路径、耗时、是否超时):
` [--version] 开始: <exe>` / ` [--version] 结束: 耗时 <ms> ms, 退出码 <c>, 超时 <0|1>, 输出 <n> 字节`
- **目录表清单与来源统计**、**缓存载入明细**(`缓存: 已载入 N 项种子`)、**逐条移除明细**
- **每轮汇总末的"下轮开始"时间戳**:`…, 耗时 <ms> ms, 下轮开始: <时间戳>`
> 目标:任何一次超过 ~2 秒没有 stderr 输出都能被察觉为可疑卡死(**需 `--verbose`**)。`--version` 探测由 5 秒超时封顶,因此探测期间的静默不超过 5 秒;扫描期间的静默由周期性进度封顶 ~1 秒(单个 `FindNextFileA`/`GetFileAttributesA` 自身阻塞超过 2 秒属 OS 层慢 I/O,本工具无法打断,见"限制")。**安静模式**下以"启动摘要 + 每轮一行"为存活信号(`--need-confirm` 时另有心跳行),单轮内的静默不做约束。
### 心跳确认(`--need-confirm <秒>`,需求 5)——独立看门狗子进程
**仅 `scan` 模式生效**(`ver` 不接该参数)。`--need-confirm x`(x 为十进制非负整数字面量,缺省/`0` = 关闭)开启后,**确认与扫盘彻底解耦**:scan 只跑自己的扫盘任务,确认交给一个**独立的看门狗子进程**。
**看门狗怎么来的**(`App/main.vp` + `App/lib/VGuard.vp`):`scan` 分派层先解析参数,若 `--need-confirm > 0` 就用 `CreateProcessA` 把**自身 exe** 拉起为隐藏子进程
```
VET.exe confirm-watch --interval <x 秒> --timeout <t 毫秒> --parent-pid <scan 的 pid>
```
- `confirm-watch` 是内部隐藏子命令,不出现在用法里;它**不做任何扫盘/目录表/缓存动作**,只跑看门狗循环。
- 父进程**不做无界等待**:拉起后只做一次**有界存活校验**(`WaitForSingleObject(hProcess, 400ms)`,见 `WATCH_ALIVE_PROBE_MS`),随后立即进入扫盘;父进程正常结束(含 `--once`)时调 `StopWatchdog()` 主动终止看门狗,杜绝孤儿进程。
- **新增代价**:每次 `scan` 启动会因这次校验**多等约 0.4 秒**(看门狗正常存活时也要等满这个窗口才继续)。这是把"拉起成功、其实没在跑"的静默降级变成致命的必要成本。
- 看门狗的标准输入/输出/错误与父进程**同一**(`STARTF_USESTDHANDLES` 指向父进程的同一批句柄,非新建管道),因此它打印的 JSON 与日志落在与 scan 相同的位置(控制台或重定向)。
**看门狗循环(本轮改动:确认输入由"阻塞读一行"改为"非阻塞取数 + deadline 驱动")**:
1. 等 **x 秒**(`WaitForSingleObject(父进程句柄, x*1000)`);若等到的不是超时而是"**父进程已退出**",看门狗立即自行退出(无需父侧配合,见下"生命周期与无孤儿")。
2. 到点:stderr 提示 `[confirm] 请在 <t> ms 内输入 {"ok": 1}(每 <x> 秒一次)` + `[confirm] 当前 <时间>, 本轮期限 <时间>`,并**以此为 deadline 起点**(`GetTickCount64`;`--verbose` 时另打一行 `[confirm] 看门狗: stdin 形态=<1|2|3>`)。
3. 在 **t 毫秒内**用"非阻塞取数"凑出完整一行并判定。stdin 形态由 `GetFileType` + `GetConsoleMode` 判定一次,分两路;**每轮循环先取数、取不到才等**(取数内部先消费"未消费输入 FIFO",见下"批量输入(快修)"一条):
- **控制台**(`FILE_TYPE_CHAR` 且 `GetConsoleMode` 成功):先把输入模式切换为"原模式清掉 `ENABLE_LINE_INPUT`/`ENABLE_ECHO_INPUT`"(掩码 `0xFFFFFFF9`,其余位——尤其 `ENABLE_PROCESSED_INPUT`(Ctrl+C)——不动),需要新数据时才取记录;本轮取不到数据时才用 `WaitForMultipleObjects({stdin, 父进程句柄}, FALSE, min(剩余, 20ms))` 等"有记录 / 父退出 / 到点",再按 `ReadConsoleInputExA` 是否可用分两路(只用 `KEY_EVENT(0x0001)` 且 `bKeyDown` 的字符:可打印 ASCII 与制表符入队,Enter 的 `\r` 归一成 `\n`,退格/方向键/非 ASCII 忽略):
- **主路径(本轮的加固 1)**:`ReadConsoleInputExA(hIn, buf, n, &read, CONSOLE_READ_NOWAIT)`——文档明写 `CONSOLE_READ_NOWAIT(0x0002)` 下"输入缓冲为空也立即返回",取数与"此刻是否有数据"解耦,**没有**"先查数量、再阻塞读"的两步组合。该导出**没有导入库/头文件**(Windows 文档 Requirements:Library none),只能动态链接,DLL 内只有 `ReadConsoleInputExA`/`ReadConsoleInputExW` 两个名字;因此本模块**动态解析**:`GetModuleHandleA("kernel32.dll")` + `GetProcAddress("ReadConsoleInputExA")`(这两个 API 本仓既有声明:`includes/w32/win32base.vp:66/65`),把地址存进 `t.Callable` 注解的函数指针变量后间接调用——**不新增任何链接期符号**,任何系统上 VET 都能加载。解析只做一次并缓存状态。
- **降级路径(该导出不存在的老系统)**:`WaitForSingleObject(stdin, min(剩余, 5ms))` → `GetNumberOfConsoleInputEvents` → `ReadConsoleInputA`。**残余竞态**:查询报出 N>0 之后若该记录被别的读者取走,`ReadConsoleInputA`(无超时参数)会等**下一条记录**;本实现只能收紧、不能根除——①这一步之前的等待不占满剩余预算(上限 `GUARD_LEGACY_WAIT_MS=5`),②剩余为 0 时直接返回、不发起这次读(绝不在已过期的 deadline 上进入阻塞读)。`--verbose` 时看门狗启动日志会打印实际走的是哪一路(`控制台取数路径=1|2`)。
- 本行判定完(成功或失败)立即写回进入前的控制台模式,并摘除控制事件处理例程(见下"控制台模式的恢复")。
- **管道 / 重定向 / 其它**:管道路径先用 `PeekNamedPipe` 取"当前真正可用的字节数",只读 `min(可用, 块上限 64B)`;非管道(`< file` 重定向等)读一块有界字节(≤64B),读到 0 字节即 EOF;当前无可读数据时按 `WaitForMultipleObjects({stdin, 父进程句柄}, FALSE, min(剩余, 20ms))` 等句柄——既监控父进程退出,又避免"读端已关闭的管道会永久置信号、可读字节却为 0"(`PeekNamedPipe` 无法区分)造成的忙等。
- **批量输入(快修)**:两路取到的**整批**字节(控制台路径为"过滤后的字符")先压入模块级**未消费输入 FIFO**(`Guard_Pend`,容量 `GUARD_PEND_CAP=128`),再由 FIFO 逐字节喂行缓冲;**一行凑满时同批剩下的字节留在 FIFO**,由下一次取数继续喂 ⇒ 一次往 stdin 写入两行以上不再出错(旧实现"一行凑满即 `return 1`"会把同批剩余字节永久丢掉)。
- 两路共用同一行累积;凑满一行后**先分类**:
- **check 请求**(对象含 `"check"` 字符串 与 `"id"` 数字,由 `VCheck.ParseCheckLine` 用仓库 json 库解析)⇒ 原样逐字节转发到 IPC 写端(补一个 `\n`);该行**不计入心跳、也不判失败**——期限继续走,用户仍需在期限内回 `{"ok": 1}`。写失败只记 stderr。
- **其余** ⇒ 交 `GuardClassifyLine` 三态分类(`ParseOkOne` 口径不变):合法 `{"ok": 1}` ⇒ 确认成功;**形状合法但值不对**(`{"ok": 0}`、`{"ok": 2}` 等)⇒ 致命失败;**形状可疑**(去掉前导空格/制表符后首字符不是 `{`、整行不含 `}`、行长 > `GUARD_OK_LINE_MAX=256`,**含空行**)⇒ stderr 回显该行(截断到 120 字符)后**忽略并继续在剩余期限内等待**,deadline 不变、不重置期限。
- **每一次等待的上限都是 `min(剩余毫秒, 20ms)`(控制台主路径的取数调用本身立即返回、不等待),到点即判超时。**
- 父进程退出(等待返回到父进程句柄那一位)⇒ 写回控制台模式后看门狗自行退出。
4. **失败**(期限到点仍未收到合法 `{"ok": 1}` / 形状合法但值不对 / 读失败 / stdin 到 EOF / stdin 句柄无效 / 控制台模式切换失败):写回控制台模式后,在 **stdout** 逐字打印唯一一行
```json
{"exit":1, "msg": "timeout/ok!=1"}
```
→ `TerminateProcess(父进程句柄)` 杀掉 scan → 看门狗自身以退出码 `1` 结束。
> **为什么必须清 `ENABLE_LINE_INPUT`(本轮的根因)**:`setconsolemode` 文档明确——行输入模式下 `ReadFile`/`ReadConsole`"**读到回车才返回**",且 `ENABLE_PROCESSED_INPUT` 只有在 `ENABLE_LINE_INPUT` **也启用**时才把回车等字符"交给系统处理、不放进输入缓冲"。旧实现等到的只是"stdin 有输入可读",随后那次 `ReadFile` 在行输入模式下会一直等到回车 ⇒ **半行 + 不回车会把看门狗挂在读里**,"t 毫秒内是否收到合法 `{"ok": 1}`"无从判定。清掉行输入后回车以 `KEY_EVENT` 记录到达本进程,判定完全由本进程的 deadline 驱动。
> **取舍:为什么连 `ENABLE_ECHO_INPUT` 一起清**:该位只作用于 `ReadFile`/`ReadConsole` 的读回显,文档并注明"仅在 `ENABLE_LINE_INPUT` 同时启用时可用";本模块根本不调用这两个函数读控制台,位留着不产生任何效果,一并清掉以免后来的人误判"还会回显"。**代价:等待期间按键不回显**(用户看不到自己敲的字符),提示行已写明应输入的内容。
**控制台记录的读取口径(`INPUT_RECORD`/`KEY_EVENT_RECORD` 布局,x64)**:
`ReadConsoleInputA` 把记录写进一段**原生字节缓冲**(`Guard_Recs`,320B = 20×16),随后按下列偏移用字节小端读取(复用 `VPe.ReadU16`/`ReadU32`,与 `VScan` 读 `WIN32_FIND_DATAA` 同一手法)。**不复用** `includes/w32/win32console.vp:77` 的 `class INPUT_RECORD`:它把 union 成员声明为嵌套结构体字段(`Event: KEY_EVENT_RECORD`),而本仓没有"对结构体值再取字段"的用例;按字节偏移读只依赖 Win32 ABI 的字段布局,不依赖编译器的结构体填充规则。
| 记录内偏移(记录步长 20B) | 字段 | 类型 | 依据 |
|---|---|---|---|
| `+0` | `EventType` | `WORD`(2B) | `INPUT_RECORD` 第 1 个字段;只对 `KEY_EVENT(0x0001)` 继续看后面的字段 |
| `+4` | `KeyEvent.bKeyDown` | `BOOL`(4B) | union `Event` 的起点 = 2B `EventType` + 2B 对齐填充 |
| `+8` | `KeyEvent.wRepeatCount` | `WORD`(2B) | `KEY_EVENT_RECORD` 第 2 个字段 |
| `+10` | `KeyEvent.wVirtualKeyCode` | `WORD`(2B) | 第 3 个字段 |
| `+12` | `KeyEvent.wVirtualScanCode` | `WORD`(2B) | 第 4 个字段 |
| `+14` | `KeyEvent.uChar` | `union{ WCHAR; CHAR }`(2B) | 第 5 个字段(UTF-16 码元;Enter = `0x000D`) |
| `+16` | `KeyEvent.dwControlKeyState` | `DWORD`(4B) | 第 6 个字段 |
| `20` | 下一条记录 | — | `sizeof(KEY_EVENT_RECORD)` = 4+2+2+2+2+4 = 16;union 内最大成员(`MOUSE_EVENT_RECORD`)也是 16 ⇒ `sizeof(INPUT_RECORD)` = 2 + 2(填充) + 16 = **20**(x86/x64 一致) |
本轮只读取 `EventType`(+0)、`bKeyDown`(+4)、`uChar`(+14) 三个偏移。
判定口径(取值口径沿用原实现,`ParseOkOne`):只认"存在 `ok` 且其值为数字 `1`",容忍 key/值与冒号之间的空格与制表符(`{ "ok" : 1 }` 通过);`{"ok": 0}`、`{"ok": 12}`、`{"ok": "1"}`、`{"ok": true}` 均不通过。`"ok"` 前一个非空白字符必须是 `{` 或 `,`,避免把字符串值里的 `ok` 误当键。
分类顺序(快修,`GuardClassifyLine`):**先做形状检查、再做取值判定**——形状不过(去掉前导空格/制表符后首字符不是 `{`、整行不含 `}`、行长 > `GUARD_OK_LINE_MAX=256`,其中**含空行**)⇒ 该行按"可疑"**忽略**(stderr 回显,截断到 120 字符)并继续在剩余期限内等待;形状通过而 `ParseOkOne` 不通过(如 `{"ok": 0}` / `{"ok": 2}`)⇒ **仍按失败收尾**("用户真的输错"这条口径不放宽)。
**参数与默认值**:
- `--need-confirm <秒>`:确认节拍(缺省/`0` = 关闭;上界 `86400`)。
- `--need-confirm-timeout <毫秒>`:确认等待超时(缺省 `5000`)。**只给 `--need-confirm-timeout` 而不给 `--need-confirm` 时不报错,但会打印一行"已忽略"告警**——理由:它只是 `--need-confirm` 的从属参数,报错会因标志顺序或复用脚本而无谓失败,告警可避免"静默无效"。看门狗未启动时该值无用。
- 看门狗**拉起失败**(例如 `GetModuleFileNameA` 失败、stdin 句柄无效、`CreateProcessA` 失败)为**致命**:打印错误并中止 scan(返回码 1)。理由:用户显式要求了心跳确认,若静默降级为"无确认"运行,等于违背安全契约。
- 看门狗**拉起成功但随即退出**同样为**致命**(本轮新增):`CreateProcessA` 返回非 0 只说明进程对象建起来了。父进程在拉起后做**存活校验**——若 400 ms 内 `WaitForSingleObject(hProcess, 400)` 返回 `WAIT_OBJECT_0`(子进程已退出),就 `GetExitCodeProcess` 取退出码打 stderr 诊断,再在 **stdout** 打同一行 `{"exit":1, "msg": "timeout/ok!=1"}`,关掉 `hProcess`/`hThread`,然后按"看门狗启动失败"中止 scan(返回码 1,`main.vp` 的既有失败分支)。返回 `WAIT_TIMEOUT` 视为存活、继续;其它等待返回值信息不足,也按存活处理(宁可少报也不误杀 scan)。**已知边界**:只观察 400 ms 这一个窗口——若看门狗"慢于 400 ms 才退出"仍会漏判,本方案不做父子握手。
**失败语义与"不写缓存"**:失败路径由**看门狗**执行 `TerminateProcess(父进程)`,scan **不做任何收尾**——不写缓存、不再输出本轮数组 JSON(避免把不完整的一轮当种子落盘;扫盘本身幂等,下一轮会重新发现并复核)。stdout 新增内容恰为上面那一行 JSON(看门狗自身失败与"拉起后立即退出"的存活校验失败打的都是这一行;后者把退出码等细节另打 stderr)。
**生命周期与"无孤儿"**(`VGuard.vp:119-125`):
| 事件 | 父进程动作 | 看门狗动作 |
|------|-----------|-----------|
| 父拉起看门狗 | `CreateProcessA`(不等待),存 `hProcess`/`hThread` | 启动,`OpenProcess(PROCESS_TERMINATE\|SYNCHRONIZE, 父 pid)` |
| 拉起后 400 ms 内看门狗已退出 | 存活校验命中:`GetExitCodeProcess` 打退出码 + stdout 打 `{"exit":1,…}` → `CloseHandle` 两个句柄 → 中止 scan(返回码 1) | 已自行退出(如 `--parent-pid` 非法 → exit 1) |
| 确认成功 | 继续扫盘 | 回到"等 x 秒"步骤,续期 |
| 确认失败 | 被 `TerminateProcess` 强杀 | 打印 `{"exit":1,…}` → 杀父 → 退出码 1 |
| 父正常结束(含 `--once`) | `StopWatchdog()`:`TerminateProcess` + `WaitForSingleObject` + `CloseHandle` | 被杀,无残留(强杀不执行用户态代码 ⇒ 若正在等待,控制台模式可能残留"已清行输入") |
| 看门狗所在终端收到 Ctrl+C / Ctrl+Break / 关窗 | 同一控制台,同样收到该事件(无处理例程 ⇒ 按默认处理终止) | 已注册的处理例程先写回控制台模式,再返回 `FALSE` 交还默认处理 ⇒ 本进程被终止(终端输入模式不再残留) |
| 父异常退出(Ctrl+C / 被强杀) | 无法执行回收 | 父进程句柄置信号 ⇒ 阶段 1 的 `WaitForSingleObject` 与阶段 2 的 `WaitForMultipleObjects` 立即返回 ⇒ 写回控制台模式后自行退出(无孤儿) |
**边界**:
- **"t 毫秒内没有输入"的准确含义**:控制台上是"t 毫秒内没有敲出并回车的一整行"(Enter 即行结束,`{"ok": 1}` 后不回车不算确认);管道/重定向上是"t 毫秒内没有以 `\n` 结束的一整行"。两路都不再有"必须等到下一次回车/下一次按键"的阻塞读。被忽略的**形状可疑行**(见下一条)不计入"收到输入"。
- **批量输入与可疑行(快修)**:一次往 stdin 写入多行时,同一批里"第一行之后"的字节**不再丢失**(整批先入未消费输入 FIFO,再逐行消费)。形状可疑的行(去掉前导空白后首字符不是 `{` / 整行不含 `}` / 行长 > 256 字节,**含空行**)只记一行 stderr 并忽略,**不判失败、不重置期限**;只有"期限到点仍未收到合法 `{"ok": 1}`"或"形状合法但值不对(如 `{"ok": 0}`)"才按失败收尾。
- **控制台等待期间按键不回显**(清 `ENABLE_ECHO_INPUT` 的代价,理由见上),且**不做行编辑**:退格、方向键、功能键不参与累积;**非 ASCII(含 IME 全角)字符被忽略** ⇒ 必须输入 ASCII 的 `{"ok": 1}`。
- **控制台模式的恢复(本轮加固 2)**:进入等待前用 `GetConsoleMode` 存原模式,**成功 / 失败 / 父进程退出**三条路径都写回(`GuardLeaveRaw`,幂等)。**确实改过模式时**同时用 `SetConsoleCtrlHandler(handler, TRUE)` 注册一个处理例程:它只做一件事——把进入前的模式 `SetConsoleMode` 写回(幂等),随后**一律返回 `FALSE` 交还默认处理**。因此 Ctrl+C / Ctrl+Break / 关窗这条"进程被默认处理终止"的路径也会先写回模式(终端不再残留"已清行输入");返回值取 `FALSE` 而非 `TRUE` 的理由:与加固前逐字同一语义(本进程仍由默认处理终止),若返回 `TRUE`("已处理")就必须自行 `ExitProcess`,否则看门狗会继续跑到下一轮 interval、再按心跳失败 `TerminateProcess` 父进程,把"用户主动 Ctrl+C"变成"杀父"。模式写回成功后立即 `SetConsoleCtrlHandler(handler, FALSE)` 摘除(注册与"已切换模式"同生命周期,故无泄漏)。**仍然无解的边界**:被 `TerminateProcess` 强杀(父进程 `StopWatchdog()` 回收、心跳失败杀父)不执行任何用户态代码,模式仍可能残留。
- **stdin 走到尽头**:`< file` 这类读盘重定向读完(`ReadFile` 读到 0 字节)⇒ 立即按失败收尾。`|` 管道写端关闭时,句柄会一直置信号而可读字节可能为 0 —— `PeekNamedPipe` 无法区分"写端仍在、暂无数据"与"写端已关、无数据",故按"暂无数据"推进,到 deadline 判**超时无输入**(旧实现把这种情况当"空行"失败;最终结果一致,只是失败时机与 stderr 原因不同)。
- **非控制台句柄一律有界读**:管道路径先问可读字节数(`PeekNamedPipe`),只读现有字节;`< file` 这类 `FILE_TYPE_DISK` 句柄无法预询可读量(`PeekNamedPipe` 只接受管道句柄),改读固定上限的块(64B),读到 0 字节即 EOF;每个"无数据"轮次另用 `WaitForMultipleObjects({stdin, 父进程句柄}, FALSE, min(剩余, 20ms))` 作为节拍(既推进循环、又监控父进程退出),不空转。
- 新看门狗的节拍与扫盘无关:即使单轮扫描远超 x 秒、或扫盘在 `Sleep(--sleep)` 长眠,看门狗仍按自己的 `WaitForSingleObject` 严格每 x 秒提示(不再受"钩子被长 Sleep 掩盖"影响)。
- 与 `--once` 的组合:`--once` 只跑一轮后父进程就 `StopWatchdog()`;若该轮**快于 x 秒**,看门狗在被回收前**来不及**发起/判定确认,因此不会出现 `{"exit":1,…}`。要观察心跳/超时,请让 scan 运行时间超过 x 秒(例如不加 `--once`,或加 `--sleep` 配合多轮)。
### 为什么是子进程而不是线程
本仓有 `includes/vthreading.vp`,但线程方案会与主流程共享同一地址空间:`VLog.Log_Buffer`(`VET/App/lib/VLog.vp:26`)与 `VScan` 的一批模块级缓冲(`Scr_*/Prog_*`,`VScan.vp:58-85`)都是进程级单例,并发写会互相踩踏;`MemBuddy` 池也非线程安全,看门狗的分配会与扫盘分配竞争。选择**子进程**的理由:内存/句柄/模块缓冲天然隔离;失败即 `TerminateProcess(父进程)`,与旧的"立即终止、不写缓存、不再打一轮 JSON"语义一致且更强;回收点清晰(父持看门狗句柄、看门狗持父进程句柄,双向兜底 ⇒ 无孤儿)。**若**改用线程,则必须为日志格式化与 `MemBuddy` 分配加锁、并把"终止整个程序"从 `sys.exit` 改为 `ExitProcess`,同时保证不写缓存——复杂度更高、收益为零,故未采用。
### check 请求通道(依附 `--need-confirm`)
`check` 是**心跳确认通道上的"顺带请求"**:只有开了心跳(看门狗子进程存在)才具备该通道,**没有独立开关**。用途是"临时点名校验一个 exe",比普通扫盘**优先级高**。
**请求**(往 **stdin** 写一行;`--need-confirm` 必须 > 0):
```json
{"check": "D:\\Viper\\VET.exe", "id": 123}
```
- `check`:必填,**字符串**,exe 的绝对路径(UTF-8 或 ANSI 均可;解析后转 ANSI 供 A 系列 API 使用)。
- `id`:必填,**整数**(JSON 数字,不是字符串)。用于消息配对,**回执原样带回同一个值**。
- 判定由**仓库 json 库解析**后按键/类型判定(不是字符串搜索):顶层是对象,且含 `"check"`(非空字符串)与 `"id"`(数字)⇒ 是 check 请求。
**回执**(scan 进程写到 **stdout**,单行 JSON,一次 `WriteFile` 写完):
```json
{"check_result":true,"id":123,"viper":{"id":"3f2504e0-4f89-11d3-9a0c-0305e82c3301","executable":"D:\\Viper\\VET.exe","version":"0.0.3-dev.428","arch":"x86_64","source":"check","displayName":"Viper 0.0.3-dev.428"}}
{"check_result":false,"id":124,"check_msg":"NOT_FOUND"}
```
- 成功:`check_result: true` + `id` + `viper`(**不含 `check_msg`**)。`viper` 字典与发现项**同构同序**:`id`/`executable`/`version`/`arch`/`source`/`displayName`(`source` 固定 `"check"` 以区分来源)。
- 失败:`check_result: false` + `id` + `check_msg`(**不含 `viper`**)。
- 键序 = 插入序,转义与 UTF-8 透传由 json 库保证(`json.object_set` + `json.write(pool, obj, False)`,与发现项数组同一套序列化)。
**失败原因(`check_msg` 只有这 9 种取值,大小写严格)**:
| `check_msg` | 含义 | 判定依据(源码路径) |
|---|---|---|
| `NOT_FOUND` | 未找到文件 | `CreateFileA` 失败且 `GetLastError ∈ {2,3}`(`ERROR_FILE_NOT_FOUND`/`ERROR_PATH_NOT_FOUND`) |
| `NO_PERMISSION` | 无权限 | `CreateFileA` 失败且 `GetLastError ∈ {5,32}`(`ERROR_ACCESS_DENIED`/`ERROR_SHARING_VIOLATION`);**其余一切打开失败取值也归此类** |
| `NOT_EXE` | 文件非 PE | 文件过短/读不出头部、缺少 `MZ`、`e_lfanew` 越界、缺少 `PE` 签名、可选头 magic 非法(`TryReadPeInfo` 的 rc 2/3/4/5 与 101–105) |
| `NO_PRVERSN` | 缺失 `.PRVERSN` 段 | rc 107(节表里没有该段名) |
| `PRVERSN_ERR` | `.PRVERSN` 段异常 | 段内容为空(首字节即 NUL)、段内容越界/段定位失败(rc 6/7/106)、整文件读取失败(rc 7)、节表超出头部窗口(rc 106) |
| `PREFIX_ERR` | `--version` 输出前缀异常 | `--version` 拉起成功(`RunVersionProbe` 返回 0)但输出前 5 字符不是 `Viper`(`VProc.OutputHasViperPrefix` 返回 0) |
| `EXE_ERR,0x<HEX>` | 拉起程序失败 | `RunVersionProbe` 返回 rc=1:`CreateProcessA` 失败(错误码 = 紧跟该调用的 `GetLastError`),另含参数/路径非法(0x0)、命令缓冲分配失败(0x0)、`CreatePipe` 失败(= 该调用的 `GetLastError`) |
| `TIMEOUT` | 超时 | `--version` 探测超过 5000 ms(`Probe_TimedOut`,已 `TerminateProcess` 回收子进程) |
| `CHANNEL_UNAVAILABLE` | check 通道自身不可用(无法执行检查) | `CheckInit` 失败(本模块的缓冲或回执工作池分配失败,`VCheck.vp` `CheckInit`);该回执由 `EmitReplyUnavailable` 直接产出,不依赖工作池,因此"请求已收到但无法检查"始终可见 |
> **十六进制格式**:`EXE_ERR,0x` 之后是**大写**十六进制、不带前导零(例:`GetLastError=2` → `EXE_ERR,0x2`;193 → `EXE_ERR,0xC1`),与 `VPe` 既有的 `unknown(0x%X)` 同一写法。
**优先级与挂起点**:scan 在**非阻塞轮询点**(`PaceDir`/`PaceFile`(每个目录项/每个候选文件)、每次发起 `--version` 探测前、每轮开始与结束)读 IPC 管道;一旦读到完整的请求行,就**同步**跑完"读 `.PRVERSN` → 执行 `--version` 验证 → 写回执"整段,期间树扫描停在那个轮询点上不前进(单线程串行,不存在与扫盘交错的窗口)。正在进行的单个 Win32 调用(`FindNextFileA`/`ReadFile` 等)与单次 `--version` 探测(≤5 秒)无法被抢占,只能在其返回后的下一个轮询点受理。
**通道怎么走的**:scan 用 `CreatePipe` 建一对管道,**读端**留在自己进程(清掉继承位 `HANDLE_FLAG_INHERIT=0`),**写端**保持可继承,并把**数值句柄**经命令行 `--ipc-write <数值>` 交给看门狗(看门狗用 `t.CPtr(数值)` 还原句柄后直接 `WriteFile`);`CreateProcessA` 成功后父进程**立即关掉自己的写端副本**(写端已由看门狗继承),因此后续 `--version` 子进程不会再继承它。看门狗把请求行**原样逐字节转发**(补一个 `\n` 作行分隔)。
**心跳语义**:转发 `check` 行**不算心跳确认、也不判失败**——期限继续走,用户仍需在期限内回 `{"ok": 1}`。转发的写失败只记 stderr(stdout/心跳契约不变);**非 check 的行交 `GuardClassifyLine` 分类**:形状合法但值不对(如 `{"ok": 0}`)仍按失败处理(打 `{"exit":1,…}` + 杀父);**形状可疑的行(含空行)只记一行 stderr 并忽略**,期限不变、继续等——只有期限到点仍未收到合法 `{"ok": 1}` 才判失败。
**入库规则**:check 命中的条目**写入 cache 并进入后续数组跟踪**(`source` = `"check"`);但若该 `executable` **已在 cache 中,则不重写**(不改其 `id`/字段,只是跳过),回执里的 `viper` 也就用既有条目的 `id`/`version`/`arch`/`displayName`(`id` 因此对同一 exe 保持稳定)。
**边界**:
- 通道**只在 `--need-confirm > 0` 时存在**(看门狗即该通道的载体);未开心跳时往 stdin 写的任何东西都没有读者。
- `id` 取值域 = json 库的 **int64**(`string.atoll`);超过 2⁶³−1 的 `uint64` 会被库截断后再回带,配对仍一致但不保证等于原文本。
- 请求行上限 4096 字节(含 NUL);更长的行会被丢弃(既不越界也不与下一行粘连)——超长路径的请求会因此判成 `NOT_FOUND`。
- 一次请求只产生**一行回执**;回执与每轮数组都写 stdout,**逐行消费**(每行都是完整 JSON,二者不会写在同一行里)。
- 启动时若打「check 通道缓冲不可用」告警,说明 check 通道缓冲/回执池分配失败(正常条件下不会出现,见「内存」一节):此时请求**仍会被受理并回执** `CHANNEL_UNAVAILABLE`,每轮重校**跳过**(不剔除任何已发现项)。
### JSON 条目字段
```json
{ "id": "uuid-string", "executable": "绝对路径", "version": ".PRVERSN内容",
"arch": "架构", "source": "workspace", "displayName": "Viper 0.0.3-dev.428" }
```
| 字段 | 说明 |
|------|------|
| `id` | 真 UUID(`ole32!CoCreateGuid`),格式 `8-4-4-4-12` 小写十六进制 |
| `executable` | 命中 exe 的绝对路径(内部为 ANSI,输出前转 UTF-8 并转义 `\` 与 `"`) |
| `version` | `.PRVERSN` 段内容(到第一个 NUL 为止) |
| `arch` | 由 PE COFF `Machine` 解析:`0x8664`→`x86_64`、`0x14c`→`x86`、`0xaa64`→`arm64`、`0x1c0`/`0x1c4`→`arm`,其余→`unknown(0x...)` |
| `source` | 条目来源:扫盘发现 = `"workspace"`;`check` 通道命中 = `"check"`(旧缓存文件缺该字段时按 `"workspace"` 载入) |
| `displayName` | `"Viper " + version` |
### 资源与节流
| 项 | 取值 |
|----|------|
| 轮次间隔 | `--sleep`,缺省 10000 ms;`0` = 不 sleep |
| 递归深度上限 | 6(相对种子目录向下的层数) |
| 跳过目录 | `WinSxS`、`$Recycle.Bin`、`System Volume Information`、`node_modules`、`.git`(大小写无关);另跳过 `.`/`..` 与**重解析点**(`FILE_ATTRIBUTE_REPARSE_POINT`,即符号链接/挂载点,防成环) |
| 节流 | 每 16 个目录项(目录侧粒度固定)或每 `--pace-units`(缺省 64)个文件 `Sleep(--pace)`(缺省 20 ms;`--pace 0` = 不 Sleep);显式给出 `--pace`/`--pace-units` 时 `--once` 下也生效;`--first-no-pace` 时第 1 轮不节流 |
| 进度输出间隔 | 每 16 个目录项或每满 1 秒一行(stderr,仅 `--verbose`) |
| `--version` 调用范围 | **仅**对已过 `PE + .PRVERSN` 过滤的 exe 调用(这类极少) |
| `--version` 超时 | **5000 ms**(`VERSION_PROBE_TIMEOUT_MS`);超时 `TerminateProcess` 并判为不通过、不入缓存 |
| PE 读取 | 先读 8 KiB 头部窗口解析节表;命中 `.PRVERSN` 才二次定位读取段内容(上限 4 KiB) |
| 每轮重校 | 每条已发现项重跑一次"PE + `.PRVERSN` + `--version`"(单条最长 5 秒;条目少时开销可忽略,条目多时会被重校拖长单轮) |
| check 回执工作池 | 72 KiB(`CK_REPLY_POOL_CAP`),单条回执在该池上的用量有固定上界预检(`CK_REPLY_NEED_FIXED + 值长度`) |
### 缓存
- **路径必须由 `--cache <path>` 传入**(缺失即报错退出)。唯一例外是 `--search-paths` 查询:它不读写缓存,因此未给 `--cache` 也允许(给了也不使用)。
- **每轮开始重读缓存**(`VCache.ReloadCache`):先清空内存表,再按文件重建(`json.parse`,文本上限 256 KiB)。文件不存在 = 正常(首次运行),按空缓存启动;打开/读取/解析失败 = **非致命**,打警告到 stderr 后按空(或尽力解析出的部分)缓存继续。
- **每轮写回**:写失败只告警不中断(下一轮会再试)。写入内容与 stdout **逐字节相同**(单行 JSON 数组)。
- **每轮完整重校**:重读后对每条已发现项做"文件存在 + PE/`.PRVERSN` 可解析 + `--version` 前 5 字符为 Viper",不合格的从缓存剔除(剔除以"本轮不写回该条"体现;重校失败不影响其余条目的写回)。
- **存在性校验**:每轮扫描后对每个缓存项 `GetFileAttributesA` 校验其 exe 是否仍存在,不存在则从结果与缓存中移除。
- **去重**:扫描到的 exe 若已在缓存中,不重复调用 `--version`;`check` 命中的 exe 若已在缓存中,不重写其 `id`/字段。
---
### 库依赖(kernel32 / shell32 / ole32)
`App/` 各模块引用的 Win32 符号及所属 DLL(均为本仓既有声明,未改 `includes/`):
| 符号 | 所属 DLL | 链接选项 | 用途 | 自声明位置 |
|------|----------|----------|------|-----------|
| `SHGetKnownFolderPath` | `shell32.dll` | `-lshell32` | 取当前用户桌面(`FOLDERID_Desktop`) | `App/lib/VDirs.vp` |
| `CoTaskMemFree` | `ole32.dll` | `-lole32` | 释放 `SHGetKnownFolderPath` 返回的 `PWSTR` | `App/lib/VDirs.vp` |
| `CoCreateGuid` | `ole32.dll` | `-lole32` | 生成发现条目的 UUID | `App/lib/VGuid.vp` |
| `GetLogicalDrives` / `GetDriveTypeA` / `GetEnvironmentVariableA` / `SetErrorMode` / `WideCharToMultiByte` | `kernel32.dll` | `-lkernel32` | 盘符枚举 / 环境变量 / 抑制临界弹窗 / 宽窄转换 | `VDirs.vp` / `VFs.vp` |
| `MultiByteToWideChar` | `kernel32.dll` | `-lkernel32` | 宽窄转换 | 既有 `includes/w32/win32base.vp:94` |
| `FindFirstFileA` / `FindNextFileA` / `FindClose` / `GetFileAttributesA` | `kernel32.dll` | `-lkernel32` | 目录枚举 / 属性查询 | 既有 `includes/w32/win32file.vp:115/117/119/99` |
| `GetStdHandle` / `WriteFile` | `kernel32.dll` | `-lkernel32` | 写 stderr(日志)与 stdout(JSON) | 既有 `win32file.vp:120/91` |
| `CreatePipe` / `SetHandleInformation` | `kernel32.dll` | `-lkernel32` | `--version` 探测的管道与继承位;心跳看门狗的标准句柄继承位;**check 通道管道**(读端清继承位、写端保持可继承) | 既有 `win32file.vp:139/140` |
| `CreateProcessA` / `TerminateProcess` / `GetExitCodeProcess` / `OpenProcess` / `GetCurrentProcessId` | `kernel32.dll` | `-lkernel32` | `--version` 子进程;看门狗子进程创建/强杀父进程/拿父进程句柄/取自进程 pid | 既有 `w32/win32process.vp:88/90/91/93/95` |
| `GetModuleFileNameA` | `kernel32.dll` | `-lkernel32` | 取自身 exe 全路径(看门狗命令行首 token) | 既有 `w32/win32file.vp:133` |
| `WaitForSingleObject` / `WaitForMultipleObjects` | `kernel32.dll` | `-lkernel32` | 超时等待(5000ms/interval)与回收(INFINITE);看门狗同时等 stdin 与父进程句柄(每次等待上限 = 本次剩余毫秒) | 既有 `w32/win32sync.vp:37/39` |
| `GetFileType` | `kernel32.dll` | `-lkernel32` | 判定 stdin 形态(控制台 / 管道 / 其它流) | 既有 `w32/win32file.vp:98` |
| `GetConsoleMode` / `SetConsoleMode` | `kernel32.dll` | `-lkernel32` | 读/写控制台输入模式(进入等待前清 `ENABLE_LINE_INPUT`/`ENABLE_ECHO_INPUT`,三条路径写回 + 控制事件例程写回) | 既有 `w32/win32console.vp:98/99` |
| `ReadConsoleInputExA` | `kernel32.dll` | —(**动态解析,不参与链接**) | 控制台非阻塞取记录(`CONSOLE_READ_NOWAIT=0x0002`);无导入库/头文件,只能 `GetProcAddress` | 本仓无声明(无导入库);VET 于 `App/lib/VGuard.vp` 用 `wb.GetModuleHandleA` + `wb.GetProcAddress` 取地址,存 `t.Callable` 变量后间接调用 |
| `GetNumberOfConsoleInputEvents` / `ReadConsoleInputA` | `kernel32.dll` | `-lkernel32` | **仅降级路径**:老系统解析不到 `ReadConsoleInputExA` 时"取未读记录数 → 只读已有记录"(残余竞态见上) | 既有 `w32/win32console.vp:102/100` |
| `SetConsoleCtrlHandler` | `kernel32.dll` | `-lkernel32` | Ctrl+C/Ctrl+Break/关窗时把进入前的控制台模式写回(处理例程只 `SetConsoleMode` + 返回 `FALSE`) | 本仓 include 未声明,VET 自声明于 `App/lib/VGuard.vp`(consoleapi.h:114-115) |
| `GetModuleHandleA` / `GetProcAddress` | `kernel32.dll` | `-lkernel32` | 动态解析 `ReadConsoleInputExA`(只取地址,不新增链接期符号) | 既有 `w32/win32base.vp:66/65` |
| `PeekNamedPipe` | `kernel32.dll` | `-lkernel32` | 管道 stdin 的"当前可读字节数"(不取走数据、立即返回);**check 通道读端的非阻塞轮询**(同一手法) | 本仓 include 未声明,VET 自声明于 `App/lib/VGuard.vp`(namedpipeapi.h) |
| `SYNCHRONIZE`(`0x00100000`) | `kernel32.dll` | `-lkernel32` | `OpenProcess` 的访问权限位(可等待) | 本仓 include 未声明,VET 自声明于 `App/lib/VGuard.vp`(winnt.h) |
| `FILE_TYPE_DISK` / `FILE_TYPE_CHAR` / `FILE_TYPE_PIPE`(`0x0001`/`0x0002`/`0x0003`) | `kernel32.dll` | `-lkernel32` | `GetFileType` 返回值判定 | 本仓 include 未声明,VET 自声明于 `App/lib/VGuard.vp`(fileapi.h) |
| `GetTickCount64` / `Sleep` / `GetLastError` / `CloseHandle` / `GetLocalTime` / `SystemTimeToFileTime` / `FileTimeToSystemTime` | `kernel32.dll` | `-lkernel32` | 计时(deadline)/ 节拍 / 错误码 / 关句柄 / 时间戳 | 既有 `win32base.vp:80/77/62/64/72/74/73` |
| `CoCreateGuid` 归属依据 | — | — | `includes/fltk/cfltk` tree 调用 `UuidCreate`(`src/drivers/WinAPI/Fl_WinAPI_System_Driver.cxx:54`),与 `CoCreateGuid` 同属 `ole32`/`combase` 的 GUID 生成族;`-lole32` 已在 `project.vpj`。**最终正确性以实际链接为准(待验证)** |
> **本次 `project.vpj` 无库变更**(`-lole32`、`-lkernel32`、`-lshell32` 均已存在)。
> 本轮新增的是**编译期源码依赖**:`includes/json/**`(`__init.vp` / `__writer.vp` / `__parser.vp`,
> 纯 Viper,已由 `includes: ["../includes"]` 覆盖),**不新增 DLL / 链接库**。
> 本轮**移除**对 `includes/subprocess.vp` 的依赖:其 `WaitForSingleObject(hProcess, INFINITE)` 无超时接口;改由 `App/lib/VProc.vp` 自实现带超时的管道捕获。
> 心跳看门狗新增的 Win32 符号(`GetModuleFileNameA` / `OpenProcess` / `GetCurrentProcessId` / `WaitForMultipleObjects` / `TerminateProcess` / `GetFileType` / `GetConsoleMode` / `SetConsoleMode` / `GetNumberOfConsoleInputEvents` / `ReadConsoleInputA` / `SetConsoleCtrlHandler` / `PeekNamedPipe`)**全部属于已链接的 `kernel32`**(`PeekNamedPipe` 在 kernel32 的导入即 `namedpipeapi.h` 声明,链接选项不变),`project.vpj` 无需改动;本仓 include 未声明的是常量 `SYNCHRONIZE`(winnt.h)、`FILE_TYPE_DISK`/`FILE_TYPE_CHAR`/`FILE_TYPE_PIPE`(fileapi.h)、`CONSOLE_READ_NOWAIT`(0x0002),以及函数 `PeekNamedPipe`、`SetConsoleCtrlHandler`(consoleapi.h)——均在 `VGuard.vp` 内以 `t.CDefine` / `extern def` 自声明。
> 本轮两项加固**不引入任何链接期符号**:`SetConsoleCtrlHandler` 走既有 `-lkernel32`;`ReadConsoleInputExA`(无导入库)走 `GetModuleHandleA` + `GetProcAddress` 动态解析,解析不到就降级为 `GetNumberOfConsoleInputEvents` + `ReadConsoleInputA`。故老系统上 VET 仍能加载运行(只是控制台取数退化为降级路径)。
---
## 目录结构
```
VET/
├── .gitignore # 忽略 output/、temp/、.tpv_build/、*.exe 等
├── LICENSE # MIT(与 Venom 同款)
├── README.md # 本文件
├── project.vpj # 项目配置(llc + clang++,输出 VET.exe)
└── App/
├── main.vp # 薄入口:控制台代码页 → 建池 → 注入各模块 _mbuddy → argv → 子命令分派
│ # (scan 分支:解析参数 → --search-paths 查询(建表打印即退出)
│ # → 拉起/回收心跳看门狗;confirm-watch 隐藏分派)
└── lib/ # 包(含 __init__.vp 包标记)
├── VLog.vp # stderr/stdout 原语 + 固定元数格式化转发 + 时间戳
├── VFs.vp # 路径/文件/通用字节串/编码(JoinPath/PathIsDir/ReadWholeFile/WriteFileAll…)
├── VPe.vp # PE/COFF 解析(ReadU16/ReadU32/节表/.PRVERSN/arch)+ ver 三段式流程
├── VGuid.vp # UUID(ole32!CoCreateGuid → 8-4-4-4-12)
├── VJson.vp # JSON 构建(仓库 json 库:单条对象 + 整轮单行数组)
├── VCache.vp # 发现缓存(内存表 + 缓存文件读写/解析(json.parse)/失效校验)
├── VDirs.vp # 常用目录表(自动构建 / --path 强制覆盖)
├── VProc.vp # 带 5s 超时的 `--version` 子进程探测(另记录"最近一次拉起失败"的阶段与 GetLastError)
├── VScan.vp # 扫盘(目录树遍历/候选处理/进度输出/一轮驱动/每轮重读+重校/check 轮询点/节流旋钮)
│ # + --search-paths 查询入口 RunSearchPathsQuery()(建表 → 逐行打印 → 返回 0)
├── VCheck.vp # check 请求通道的执行侧:请求行解析(json 库)/8 种失败原因映射/命中入库/单行 JSON 回执
├── VGuard.vp # 心跳看门狗:父侧拉起/回收自身子进程 + 子进程 confirm-watch 循环 + check 通道 IPC 管道
│ # (确认输入为 deadline 驱动的非阻塞取数:控制台走动态解析的 ReadConsoleInputExA + CONSOLE_READ_NOWAIT,
│ # 并由 SetConsoleCtrlHandler 兜住模式写回;check 行经 IPC 原样转发给父进程)
└── VCli.vp # 命令行解析(scan/ver、--cache/--once/--sleep/--pace/--pace-units/--need-confirm[-timeout]/
# --verbose/--path/--search-paths、confirm-watch 的 --ipc-write)+ 用法文本
```
> 跨模块共享内存池沿用本仓惯例:各模块声明模块级 `_mbuddy: rawptr[memhub.MemBuddy]`,由 `App/main.vp` 建池后逐个注入(同一 MemBuddy 实例);不使用 varargs 自定义函数。
---
## 当前能力(已验证 / 未验证边界)
> 本仓库交付时 **未编译、未运行**,所有结论均为静态论证。以下标注"已验证"指"已由源码/规范静态论证"。
- **已验证(静态)**:
- 取参方式 `sys._init_argv()` + `sys._argc` / `sys._argv`(`includes/sys.vp:67`、`includes/sys.vp:60-64`)。
- 文件读取走 `w32/win32file.vp` 的 `CreateFileA` / `GetFileSizeEx` / `ReadFile` / `SetFilePointerEx` / `WriteFile` / `CloseHandle`(`win32file.vp:88/95/90/93/91`、`win32base.vp:64`)。
- 目录枚举走 `FindFirstFileA` / `FindNextFileA` / `FindClose`(`win32file.vp:115/117/119`),返回结构用**裸 320 字节缓冲 + 偏移读取**(`dwFileAttributes`@0、`cFileName`@44),不依赖 Viper 结构体打包。
- stdout/stderr 走 `GetStdHandle(STD_OUTPUT_HANDLE/STD_ERROR_HANDLE)`(`win32file.vp:120/45/46`)+ `WriteFile`(`:91`)。
- PE 手工解析的偏移量按 PE/COFF 规范(详见交付报告"静态论证"一节)。
- `--version` 子进程捕获由 `App/lib/VProc.vp` 自实现:`CreatePipe` + 裸缓冲 `SECURITY_ATTRIBUTES`/`STARTUPINFOA`/`PROCESS_INFORMATION`(偏移写入)+ `CreateProcessA` + `WaitForSingleObject(5000)`(超时 `TerminateProcess`)+ 读干管道 + 全句柄 `CloseHandle`;不再使用 `includes/subprocess.vp`。
- UUID 走 `ole32!CoCreateGuid`;GUID 小端布局(Data1 u32 / Data2 u16 / Data3 u16 / Data4[8])与 `wb.GUID`(`win32base.vp:50-54`)字段宽度一致,按数值格式化后即 `8-4-4-4-12`。
- 编码:枚举与文件 API 统一走 A 系列(系统 ANSI),JSON 输出前经 `MultiByteToWideChar(CP_ACP)→WideCharToMultiByte(CP_UTF8)` 转 UTF-8。
- **check 通道**(`App/lib/VCheck.vp` + `VGuard.PollIpc`):请求行用仓库 json 库解析(按 `"check"` 字符串 + `"id"` 数字判定,不做字符串搜索);回执用 `json.object_set` + `json.write(pool, obj, False)` 序列化为单行;失败原因 9 种取值(8 种来自判定 = `TryReadPeInfo` 的 rc 表 + `CreateFileA`/`CreateProcessA` 的 `GetLastError` + `Probe_TimedOut` + `OutputHasViperPrefix`,第 9 种是通道自身不可用 `CHANNEL_UNAVAILABLE`)的映射见「check 请求通道」对照表。
- check 命中/重校与扫盘共用同一套判定(`VPe.TryReadPeInfo` + `VProc.RunVersionProbe`),"文件存在 + PE/`.PRVERSN` + 前 5 字符 `Viper`"只有一个实现点(`VCheck.CheckExecutable`)。
- **待验证(需实际编译运行)**:
- 各模块编译通过性(交付时未编译);跨模块 `t.CDefine`(如 `VPe.VER_CAP`)、跨模块 `extern` 引用与多文件项目(`App/lib/*.vp` 自动递归收集)的解析。
- `CoCreateGuid` 的实际链接(`-lole32`)与返回 UUID 的格式/取值。
- `VProc` 的 `CreateProcessA`/管道捕获在 `VET` 自身进程内的实际行为、`STARTUPINFOA`/`PROCESS_INFORMATION`/`SECURITY_ATTRIBUTES` 裸偏移写入的正确性(ctypes 已只读核验尺寸/偏移,见交付报告)。
- 超时路径(`WaitForSingleObject` 返回 5000ms 超时 → `TerminateProcess`)在真实挂起 exe 下的行为。
- 心跳看门狗(`App/lib/VGuard.vp`,本轮重构)的实际运行行为:`GetModuleFileNameA(NULL,…)` 取自身全路径、`CreateProcessA` 以 `STARTF_USESTDHANDLES` 复用父进程 stdin/stdout/stderr、`OpenProcess(PROCESS_TERMINATE|SYNCHRONIZE, 父 pid)`、`WaitForSingleObject`/`WaitForMultipleObjects` 对**控制台输入句柄**的信号语义、`ReadFile` 按行读取(含 `\r\n` 处理)、`TerminateProcess(父进程)` 的强杀、以及父正常结束时 `StopWatchdog()` 的回收。引用符号:`wf.GetModuleFileNameA`(`win32file.vp:133`)、`wf.GetStdHandle`(`:120`)、`wf.ReadFile`(`:90`)、`wf.SetHandleInformation`(`:140`)、`wsync.WaitForSingleObject`/`WaitForMultipleObjects`(`win32sync.vp:37/39`)、`wp.CreateProcessA`/`TerminateProcess`/`OpenProcess`/`GetCurrentProcessId`(`win32process.vp:88/90/93/95`)、`wb.CloseHandle/GetLastError`(`win32base.vp:64/62`)、`sys.exit`(`sys.vp:26`),均为本仓既有声明(**无新增 DLL/库依赖**;仅 `SYNCHRONIZE` 常量自声明)。
- `STARTF_USESTDHANDLES` 复用控制台句柄时,句柄必须可继承(VET 用 `SetHandleInformation(...HANDLE_FLAG_INHERIT...)` 尽力设置、失败忽略);若某终端/重定向下句柄不可继承,`CreateProcessA` 会失败——VET 会打印 `GetLastError` 并**中止 scan**(**待确认**:真实控制台/`>` 重定向/管道三种场景下的句柄可继承性)。
- **check 通道的端到端行为**:`--ipc-write <句柄数值>` 的 ptrtoint/inttoptr 往返(`t.CUInt64T(HANDLE)` → `%llu` → `VCli.ParseUint64` → `t.CPtr(数值)`)、管道写端确实被看门狗继承、父进程非阻塞轮询确实不阻塞、回执在真实 stdout 上的单行性(**待确认**;本文档给出的所有保证均为静态论证)。
- `%llu` 变参格式化(`viperlib.snprintf`)在 `t.CUInt64T` 实参下的位宽正确性:同形用法已在本仓多处存在(`includes/memhub.vp:930-932` 的 `pool_base=%llu` 配 `t.CUInt64T(self.usable)`),故按既有先例采用(**待确认**:句柄值高位为 1 时能否原样往返)。
- `.PRVERSN` 段在真实产物中的存在性(仅声明了该段的 exe 会有)。
- `WIN32_FIND_DATAA` 的 `cFileName`@44 偏移在真实 `kernel32!FindFirstFileA` 下的正确性。
- `SHGetKnownFolderPath` / `CoTaskMemFree` / `WideCharToMultiByte` / `SetErrorMode` 的实际运行行为。
- `SetErrorMode(SEM_FAILCRITICALERRORS)` 对本进程后续文件操作的实际影响。
- `GetLocalTime`/`SystemTimeToFileTime`/`FileTimeToSystemTime` 的时间戳(含 FILETIME 100ns 加法)正确性。
- **已知偏差(重要)**:仓库内现有预编译产物 `exe/TransPyV.exe` **并不包含** `.PRVERSN` 段——
实测其段表为 `.text/.rdata/.buildid/.data/.pdata/.INTR/.WEAK/.reloc`,
版本串 `0.0.3-dev.428` 落在 `.data` 段内(源码头 `TransPyV/App/main.vp:45` 虽声明了
`t.attr.section(".PRVERSN")`)。因此对**该**产物运行 VET,`[1/3]` 会输出"未找到 .PRVERSN 节"
并列出实际节名,`scan` 也不会命中它。这是产物/工具链侧的偏差,不是 VET 的解析缺陷;若工具链
已改为真正发射该段,或重新构建 TransPyV,则应能命中。
---
## 限制
- **仅 Windows / PE32 / PE32+**:解析的是 Windows PE 段表,不支持 ELF/Mach-O。
- **仅校验首个匹配的 `.PRVERSN` 段**:不处理多个同名段(现实中不会有)。
- **`--version` 校验只认前 5 字符 `Viper`**:不做语义化版本比较。
- **`--version` 超时 5 秒**:`App/lib/VProc.vp` 用 `WaitForSingleObject(hProcess, 5000)`;超时即 `TerminateProcess` 并判该 exe 不通过(不入缓存)。此前用 `subprocess.run`(`WaitForSingleObject(INFINITE)`)无超时,被探测 exe 挂起会卡住整轮——此为**行为增强**。
- **先等进程、后读管道**:为拿到"进程已退出/已杀"的确定态,`VProc` 先 `WaitForSingleObject(5000)` 再读干管道。因此若某 exe 的 `--version` 输出**超过管道缓冲(约 64 KiB)**,子进程会写阻塞、父进程等超时后强杀,导致该 exe 被判超时。`--version` 输出远小于此,实践无影响(已记录为限制)。
- **stderr 合并进 stdout**:`VProc` 把 `hStdError` 与 `hStdOutput` 接到同一管道(便于诊断),因此"前 5 字符为 `Viper`"校验对混入前置 stderr 的 exe 会失败(与旧 `subprocess` 行为一致,`subprocess.vp:104`)。
- **进度不能打断单个 API**:进度行由扫描循环驱动;若单个 `FindNextFileA`/`GetFileAttributesA` 自身阻塞(慢 I/O/网络盘)超过 2 秒,工具无法打断(无看门狗线程)。`--version` 探测则由 5 秒超时封顶。
- **心跳确认 = 独立看门狗子进程**:`scan --need-confirm x` 会用 `CreateProcessA` 拉起 `VET.exe confirm-watch …`,与扫盘完全并行、`WaitForSingleObject`/`WaitForMultipleObjects` 驱动,**严格每 x 秒**提示(不再受扫盘钩子/长 `Sleep` 影响)。确认窗口内是**非阻塞取数**:控制台走 `ReadConsoleInputExA` + `CONSOLE_READ_NOWAIT`(动态解析,老系统退化为"查数量→`ReadConsoleInputA`"并有残余竞态),管道按 `PeekNamedPipe` 的可读字节数读,重定向文件按有界块读;取到的整批先入**未消费输入 FIFO**(`GUARD_PEND_CAP=128`,同批剩余字节不丢),每次等待的上限都是 `min(本次剩余毫秒, 20ms)`,**期限到点或形状合法但值不对**即判失败(不再出现"半行不回车把看门狗挂在 `ReadFile` 里";形状可疑的行只记 stderr 并忽略)。代价:多一个常驻进程、多一次 `CreateProcessA`(细节与"无孤儿"保证见「心跳确认」)。
- **等待期间按键不回显、不做行编辑**:控制台路径清掉 `ENABLE_LINE_INPUT`/`ENABLE_ECHO_INPUT`(理由与代价见「心跳确认」);退格/方向键/非 ASCII 被忽略,需输入 ASCII 的 `{"ok": 1}`。Ctrl+C / Ctrl+Break / 关窗时由注册的 `SetConsoleCtrlHandler` 处理例程写回模式(终端不再残留"已清行输入");**只有被 `TerminateProcess` 强杀**(父进程回收看门狗、心跳失败杀父)不执行用户态代码、模式仍可能残留。
- **心跳失败不写缓存**:失败路径由看门狗 `TerminateProcess(父进程)` 强杀 scan,父进程**不做任何收尾** ⇒ 本轮尚未落盘的命中项丢失(下一轮会重新发现并复核,故幂等安全);stdout 恰好多出那一行 `{"exit":1,…}`。
- **心跳看门狗拉起失败 = 致命**:`--need-confirm > 0` 时若 `GetModuleFileNameA`/`stdin` 句柄/`CreateProcessA` 任一失败,scan 打印错误并以返回码 1 中止(不静默降级为"无确认")。
- **`--once` + `--need-confirm`**:单轮快于 x 秒时看门狗来不及判定就被回收,看不到确认提示/退出 JSON;要观察需让 scan 活过 x 秒。
- **看门狗"拿不到父进程句柄"时不打印退出 JSON**:`OpenProcess` 失败(现实几乎不可能,同用户同会话)时看门狗只在 stderr 记一行并以 1 退出(既不杀父也不写 JSON,因为无法履行契约)。
- **进度行可能较密集**(仅 `--verbose`):每 16 个目录项即一行(沿用 `PACE_DIR_UNITS`),大量小目录下 stderr 行数较多;如需更稀疏可调 `VScan.PACE_DIR_UNITS` 或仅依赖 1 秒阈值。注意 `--pace-units` **不改变**进度行密度(它只改文件侧节流粒度,进度输出规则独立)。默认安静模式不输出进度行,不受此影响。
- **安静模式单轮内无进度**:默认只打启动摘要与每轮一行,单轮内的长时间无输出属正常现象;要观察单轮进展需 `--verbose`(开启 `--need-confirm` 时另可按心跳行判断活跃)。
- **`--search-paths` 是"建表时刻的快照"**:打印后即退出,之后的路径增删、盘符热插拔都不会反映;其"是否会实际扫"标注只是建表后对表内条目各做一次属性查询(`GetFileAttributesA`)的结果,与真正开始扫盘之间若目录消失,扫盘仍会照旧跳过(标注可能因此与实际略有偏差)。它也不给出逐条来源(静态/盘符枚举/桌面)——`VDirs` 表内不保存该标记。另外该查询**不读缓存也不扫盘**,因此它的输出是"计划搜索的根目录",不是"本轮命中的 exe"。
- **路径长度受 `MAX_PATH`(A 系列 API)限制**:完整路径超过约 512 字节(`PATH_BUF_CAP`)时 `JoinPath` 直接跳过该项。
- **PE 头部窗口 8 KiB**:若某 exe 的节表起点超过 8 KiB(极端多节 PE),将被跳过而非误判。
- **JSON 输出缓冲 1 MiB**:一轮条目文本超出时**跳过放不下的条目**(仍保证是合法 JSON,末尾闭合 `]`),并告警。
- **单条 JSON 上限 4096 字节**:json 库的 `write` 使用固定 4096 字节输出缓冲且无容量参数
(`includes/json/__writer.vp:114`),VET 侧在序列化前用"转义最坏翻倍"的保守上界预检
(`VJson.JSON_ENTRY_MAX`),超限条目**跳过并告警**(不会写穿缓冲)。
- **缓存文本解析上限 256 KiB**:超过(`VCache.CACHE_TEXT_MAX`)或解析失败一律按"空缓存启动"
并告警;解析工作池按 `CACHE_ARENA_PER_BYTE`(128 字节/输入字节)保守预留,保证不会中途耗尽。
- **缓存不做增量合并**:每轮以"重读缓存文件 + 完整重校 + 重新扫盘 + 存在性校验"重建结果;不在缓存中做基于 mtime/版本的差异合并。
- **check 通道依附心跳**:只在 `--need-confirm > 0` 时存在;没有心跳就没有看门狗,也就没有请求的读者与转发方(往 stdin 写什么都不会被处理)。未开该通道时 scan 的 stdout 只有每轮数组。
- **check 与扫盘共用单线程**:check 执行期间扫盘停在轮询点(这正是"check 优先级更高"的实现方式)。因此**单个 check 最长可占用 ~5 秒**(`--version` 探测超时上限),且只有当扫盘走到下一个轮询点时才被受理——正在进行的单个 Win32 调用与单次探测不可抢占。
- **每轮完整重校会重跑 `--version`**:缓存里每条已发现项每轮都要重新打开/解析/拉起一次 `--version`(单条最长 5 秒)。条目多时单轮耗时明显上升;这是"完整重校"的直接代价(不做按 mtime/版本号的短路)。
- **每轮重读缓存会让"上一轮写盘失败"的进程内条目丢失**:`ReloadCache` 先清空内存表再按文件重建,若上一轮 `WriteCacheFile` 失败,那一轮的命中项在本轮开盘就不在表里——由"扫盘会重新发现、check 会重新命中"兜底(扫盘幂等),缓存文件本身保持"最后成功写入的那份"。
- **check 请求行上限 4096 字节**:更长的行被丢弃(看门狗侧缓冲上限 `GUARD_LINE_CAP` 与父侧 `IPC_LINE_CAP` 同值);超长路径即便合法也会因行被丢弃而无回执,或(若路径被截断成合法 JSON 的短串)判成 `NOT_FOUND`。
- **`id` 受 json 库的 int64 域限制**:`string.atoll` 解析,超过 2⁶³−1 的 `uint64` 在解析/回带时被截断(配对仍自身一致)。
- **check 通道的管道容量是"提示值"**:`CreatePipe` 的 `nSize` 传 65536,实际缓冲由系统决定;短时大量请求若超出管道容量,看门狗的 `WriteFile` 可能短暂阻塞(父进程在每个轮询点排空,实践中不易触发)。
---
## 许可
MIT,见 [LICENSE](./LICENSE)。
+40
View File
@@ -0,0 +1,40 @@
{
"name": "VET",
"version": "1.0.0",
"source_dir": "./App",
"build_dir": "./.tpv_build",
"compiler": {
"cmd": "llc",
"flags": [
"-filetype=obj",
"-relocation-model=pic"
]
},
"linker": {
"cmd": "D:/msys64/mingw64/bin/clang++.exe",
"flags": [
"-fuse-ld=lld",
"-lmsvcrt",
"-lucrt",
"-lpthread",
"-lmingwex",
"-lkernel32",
"-lshell32",
"-lole32",
"-Wl,--allow-multiple-definition"
],
"output": "VET.exe"
},
"includes": [
"../includes"
],
"target": {
"triple": "x86_64-pc-windows-gnu",
"datalayout": "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
},
"options": {
"slice_level": 3,
"target": "llvm",
"strict_mode": true
}
}