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:
commit
02df100204
19 files changed
+6160
No files matched your search
+13
@@ -0,0 +1,13 @@
|
|||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.exe
|
||||||
|
*.img
|
||||||
|
*.iso
|
||||||
|
**/__pycache__/
|
||||||
|
.cache/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# 编译
|
||||||
|
**/output/
|
||||||
|
**/temp/
|
||||||
|
**/.tpv_build/
|
||||||
@@ -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"}]
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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 的字段声明一致。
|
||||||
|
# ============================================================
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user