chore(comments): VET 注释英文化(R11)
14 文件中文 # 注释 -> 简洁英文(+1034/-1695,净 -661 行):每个连续注释块压到 <=2 行,保留作用/约束/危险警告/ABI 与类型约定/内存与句柄所有权,删除历史沿革与逐字复述;头注释一并英文化(VET 头部很短,与 includes 批次的 547 行大块头注释不同)。运行时中文字符串(VLog/printf/fatal_error 实参)按约束保持原样。 独立验收:_r11check 全量骨架比对 14 文件漂移 0;中文注释残留 0;VET 编译 SUCCESS + --search-paths 7 条目录表、exit 0。
This commit is contained in:
1 parent
af05cd0b53
commit
78a7897d30
14 files changed
+1022
-1683
No files matched your search
+42
-72
@@ -11,38 +11,30 @@ import w32.win32base as wb
|
||||
|
||||
|
||||
# ============================================================
|
||||
# VCache - 发现缓存(内存态 + 缓存文件读写/解析/失效校验)
|
||||
# VCache - discovery cache (in-memory table + cache file read/write/parse/validation)
|
||||
#
|
||||
# 从原 App/main.vp 平移 Cache*/LoadCache/WriteCacheFile/ParseCacheText/ValidateCache。
|
||||
# 与 VJson 解耦:WriteCacheFile 不再直接读 VJson 的 Json_Buf,而是由调用方把
|
||||
# (Buf, Len)传入;表项读取经 CacheCount()/CacheExe(i)… 暴露,避免 VJson↔VCache 环。
|
||||
# Decoupled from VJson: WriteCacheFile takes (Buf, Len) from the caller instead of reading
|
||||
# VJson's buffer; entries are exposed via CacheCount()/CacheExe(i)... to avoid an import cycle.
|
||||
#
|
||||
# 日志详细度:缓存载入/逐条移除明细("已载入 N 项种子"、"[移除] 已不存在: <路径>"、
|
||||
# "未找到/无法打开: <路径>")属过程细节,走 VLog.Verbose*;解析/读取失败等降级说明
|
||||
# (文本超限、工作池预检不足、JSON 解析失败、写入失败)始终输出且不内嵌路径。
|
||||
# Verbosity: load/per-entry-removal detail goes through VLog.Verbose*; parse/read fallback
|
||||
# messages (text too long, pool precheck short, JSON parse failure, write failure) always print.
|
||||
#
|
||||
# 本轮改造(需求 ②):解析不再用自写的"极小 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)。
|
||||
# Parsing uses the repo json library json.parse, then copies fields into the C-side table.
|
||||
# Memory ownership is documented at ParseCacheText.
|
||||
# ============================================================
|
||||
|
||||
define CACHE_CAP = 256 # 缓存条目上限
|
||||
define CACHE_FILE_MAX = 16777216 # 缓存文件读取上限 16 MiB
|
||||
define DISP_CAP = 160 # "Viper " + 版本串
|
||||
define CACHE_TEXT_MAX = 262144 # 解析文本上限 256 KiB(典型缓存约 60 KiB;超出即视为损坏/异常,按空缓存启动)
|
||||
define CACHE_ARENA_PER_BYTE = 128 # 解析工作池容量 / 输入字节(保守上界,见 ParseCacheText)
|
||||
define CACHE_ARENA_SLACK = 4096 # 解析工作池固定余量
|
||||
define CACHE_CAP = 256 # cache entry limit
|
||||
define CACHE_FILE_MAX = 16777216 # cache file read limit 16 MiB
|
||||
define DISP_CAP = 160 # "Viper " + version string
|
||||
define CACHE_TEXT_MAX = 262144 # parse text limit 256 KiB (typical ~60 KiB; larger = corrupt, start empty)
|
||||
define CACHE_ARENA_PER_BYTE = 128 # parse working pool capacity per input byte (upper bound, see ParseCacheText)
|
||||
define CACHE_ARENA_SLACK = 4096 # parse working pool fixed slack
|
||||
|
||||
# 由 App/main.vp 注入
|
||||
# Injected by App/main.vp
|
||||
_mbuddy: memhub.MemBuddy*
|
||||
|
||||
# 发现缓存(内存态;ANSI 路径,用于与 FindFirstFileA 枚举结果直接比对)
|
||||
# Cache_Src 记录条目来源("workspace" = 扫盘发现;"check" = check 通道命中),
|
||||
# 是 JSON 条目 source 字段的唯一来源(VJson.BuildEntry 读 CacheSrc)。
|
||||
# Discovery cache (in-memory; ANSI paths, directly comparable to FindFirstFileA results).
|
||||
# Cache_Src holds the entry source ("workspace"=scan found, "check"=check hit); its only origin.
|
||||
Cache_Exe: t.CArray[str, CACHE_CAP]
|
||||
Cache_Id: t.CArray[str, CACHE_CAP]
|
||||
Cache_Ver: t.CArray[str, CACHE_CAP]
|
||||
@@ -51,12 +43,12 @@ 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 literal: passed in by the scan side (VScan) / check side (VCheck); cache does not interpret it.
|
||||
# Cache files without the source field load as SOURCE_FALLBACK, keeping existing output unchanged.
|
||||
define SOURCE_FALLBACK = "workspace"
|
||||
|
||||
|
||||
# ---- 表读取接口(供 VJson 构建 JSON,不暴露数组本身) ----
|
||||
# ---- table read API (for VJson to build JSON, without exposing the arrays) ----
|
||||
def CacheCount() -> int:
|
||||
return Cache_Count
|
||||
|
||||
@@ -86,7 +78,7 @@ def CacheDisp(Idx: int) -> str:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 内存缓存
|
||||
# In-memory cache
|
||||
# ============================================================
|
||||
|
||||
def CacheFind(ExeAnsi: str) -> int:
|
||||
@@ -105,9 +97,8 @@ def FreeEntry(Idx: int):
|
||||
_mbuddy.free(t.CPtr(Cache_Disp[Idx]))
|
||||
|
||||
|
||||
# 移除第 Idx 条(释放各字段副本并把后续条目左移一位)。返回 1=已移除。
|
||||
# 单点实现:ValidateCache(存在性校验)与 VScan 的"每轮完整重校"都经此处剔除条目,
|
||||
# 避免两处各写一遍左移逻辑。
|
||||
# Remove entry Idx (frees field copies and shifts later entries left). Returns 1=removed.
|
||||
# Single implementation shared by ValidateCache and VScan's per-round revalidation.
|
||||
def RemoveAt(Idx: int) -> int:
|
||||
global Cache_Count
|
||||
if Idx < 0 or Idx >= Cache_Count:
|
||||
@@ -126,7 +117,7 @@ def RemoveAt(Idx: int) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 清空内存表(释放全部字段副本)。用于每轮开始前的"重读 cache.json"。
|
||||
# Clear the in-memory table (frees all field copies). Used before re-reading cache.json each round.
|
||||
def ClearCache():
|
||||
global Cache_Count
|
||||
for j in range(Cache_Count):
|
||||
@@ -134,8 +125,8 @@ def ClearCache():
|
||||
Cache_Count = 0
|
||||
|
||||
|
||||
# 把一条发现记录拷入缓存(各字段独立副本)。返回 1=成功,0=容量满/分配失败
|
||||
# Src:条目来源("workspace"/"check"),原样存入并在 JSON 输出时透传。
|
||||
# Copy one discovery record into the cache (each field duplicated). 1=ok, 0=full/alloc failure.
|
||||
# Src: entry source ("workspace"/"check"), stored as-is and passed through on JSON output.
|
||||
def CacheAdd(ExeAnsi: str, Id: str, Ver: str, Arch: str, Src: str, Disp: str) -> int:
|
||||
global Cache_Count
|
||||
if Cache_Count >= CACHE_CAP:
|
||||
@@ -184,7 +175,7 @@ def CacheAdd(ExeAnsi: str, Id: str, Ver: str, Arch: str, Src: str, Disp: str) ->
|
||||
return 1
|
||||
|
||||
|
||||
# 校验缓存项对应的 exe 是否仍存在:不存在则从缓存移除(返回移除数)
|
||||
# Drop cache entries whose exe no longer exists; returns the number removed
|
||||
def ValidateCache() -> int:
|
||||
removed: int = 0
|
||||
j: int = 0
|
||||
@@ -199,10 +190,10 @@ def ValidateCache() -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 缓存文件
|
||||
# Cache file
|
||||
# ============================================================
|
||||
|
||||
# 把 EmitJson 生成的文本(Buf,共 Len 字节)写入缓存文件(失败只告警,不中断进程)
|
||||
# Write EmitJson text (Buf, Len bytes) to the cache file (failure only warns, does not abort)
|
||||
def WriteCacheFile(Path: str, Buf: bytes, Len: sizet) -> int:
|
||||
if Buf is None:
|
||||
return 0
|
||||
@@ -216,8 +207,8 @@ def WriteCacheFile(Path: str, Buf: bytes, Len: sizet) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 取对象里指定键的字符串值;键缺失/非字符串/值为空指针 ⇒ 空串
|
||||
# (与原实现"缺字段记 ''"一致)。纯只读字段遍历,先例见 includes/_dict.vp:241-261。
|
||||
# Return the string value for Key in Obj; missing / non-string / null => empty string.
|
||||
# Read-only field walk.
|
||||
def FieldStr(Obj: json.JsonValue*, Key: str) -> str:
|
||||
cu: json.JsonValue* = Obj.child
|
||||
while cu != None:
|
||||
@@ -230,9 +221,8 @@ def FieldStr(Obj: json.JsonValue*, Key: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
# 把一个解析出的 JSON 对象搬进缓存表(逐字段取出 → CacheAdd 在 Vet_Mb 上复制持久副本)。
|
||||
# 需求 executable 与 id 非空才计入(与原实现 "TmpExe[0]!=0 and TmpId[0]!=0" 等价)。
|
||||
# source 缺失/非字符串/空串 ⇒ SOURCE_FALLBACK(旧缓存文件无该字段,输出与既有行为一致)。
|
||||
# Move a parsed JSON object into the cache table (fields read out, CacheAdd makes persistent copies).
|
||||
# Requires non-empty executable and id; missing/empty source => SOURCE_FALLBACK.
|
||||
def AddParsedEntry(Obj: json.JsonValue*, ConvBuf: bytes) -> int:
|
||||
Exe: str = FieldStr(Obj, "executable")
|
||||
Id: str = FieldStr(Obj, "id")
|
||||
@@ -241,28 +231,15 @@ def AddParsedEntry(Obj: json.JsonValue*, ConvBuf: bytes) -> int:
|
||||
Src: str = FieldStr(Obj, "source")
|
||||
if string.strlen(Src) == 0:
|
||||
Src = SOURCE_FALLBACK
|
||||
# 与扫描侧口径一致:缓存内 executable 存 ANSI(CacheFind 要与 FindFirstFileA 的 ANSI 结果比对)
|
||||
# Match the scan side: store executable as ANSI (CacheFind compares with FindFirstFileA ANSI results)
|
||||
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。
|
||||
# Parse cache text (top-level array of objects with string fields) via json.parse; returns entries added.
|
||||
# Memory: the tree lives on a per-call MemPool arena (freed whole); CacheAdd copies onto Vet_Mb.
|
||||
def ParseCacheText(Txt: str) -> int:
|
||||
if Txt is None:
|
||||
return 0
|
||||
@@ -273,10 +250,8 @@ def ParseCacheText(Txt: str) -> int:
|
||||
VLog.LogFmt2("缓存: 文本 %zu 字节超出解析上限 %zu,按空缓存启动\n",
|
||||
L, sizet(CACHE_TEXT_MAX))
|
||||
return 0
|
||||
# 工作池容量:解析树最坏 ≈ 每输入字节 1 个 JsonValue 节点(9 字段 × 8 = 72 字节上界,bump 池
|
||||
# 再按 8 字节对齐)+ 每字符串一次键名副本与值副本 + 数字/键名临时缓冲(最坏 ≈ 80 字节/输入字节);
|
||||
# 按 CACHE_ARENA_PER_BYTE(128)再放大,覆盖每次分配的 8 字节对齐浪费。
|
||||
# (MemPool 耗尽会 printf + exit(1),故必须一次给足;这是不选 MemPool 小池的原因。)
|
||||
# Pool capacity: worst case ~1 JsonValue node per input byte + key/value copies + temps, scaled
|
||||
# by CACHE_ARENA_PER_BYTE(128) to cover 8-byte alignment. A depleted MemPool would exit(1).
|
||||
Cap: sizet = L * sizet(CACHE_ARENA_PER_BYTE) + sizet(CACHE_ARENA_SLACK)
|
||||
Arena: bytes = stdlib.malloc(Cap)
|
||||
if Arena is None:
|
||||
@@ -287,11 +262,8 @@ def ParseCacheText(Txt: str) -> int:
|
||||
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)。
|
||||
# Pool precheck (fail-closed): the whole tree must fit, else json.parse's pool.alloc would
|
||||
# exit(1). Need mirrors Cap, so this fails cleanly (empty-cache fallback) if Cap is too small.
|
||||
Need: sizet = L * sizet(CACHE_ARENA_PER_BYTE) + sizet(CACHE_ARENA_SLACK)
|
||||
if Pool.AllocOk(Need) == 0:
|
||||
stdlib.free(t.CPtr(Arena))
|
||||
@@ -320,7 +292,7 @@ def ParseCacheText(Txt: str) -> int:
|
||||
return added
|
||||
|
||||
|
||||
# 启动时读缓存做种子。文件不存在/读失败/解析失败均为非致命:告警后按空缓存启动。
|
||||
# Read the cache as a seed at startup. Missing/unreadable/unparsable are non-fatal: warn, start empty.
|
||||
def LoadCache(Path: str) -> int:
|
||||
buf: bytes = VFs.ReadWholeFile(Path, sizet(CACHE_FILE_MAX),
|
||||
wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE)
|
||||
@@ -341,10 +313,8 @@ def LoadCache(Path: str) -> int:
|
||||
return n
|
||||
|
||||
|
||||
# 每轮开始前的"重读 cache.json":先清空内存表(释放上一轮的字段副本),再按文件重建。
|
||||
# 返回本次载入的条目数。文件不存在/读取失败/解析失败均按空缓存继续(与 LoadCache 同一口径,
|
||||
# 由 LoadCache 内部告警);代价是"上一轮写盘失败时进程内已知条目会丢失"——为此把写盘失败
|
||||
# 只降级为告警(WriteCacheFile),并由"下一轮树扫描会重新发现"兜底(扫盘幂等)。
|
||||
# Re-read cache.json at round start: clear the in-memory table, then rebuild from the file.
|
||||
# Returns entries loaded; missing/unreadable/unparsable continue empty (LoadCache warns internally).
|
||||
def ReloadCache(Path: str) -> int:
|
||||
ClearCache()
|
||||
return LoadCache(Path)
|
||||
+140
-195
@@ -14,151 +14,121 @@ import lib.VCache as VCache
|
||||
|
||||
|
||||
# ============================================================
|
||||
# VCheck - check 请求通道的"执行 + 回执"侧(父进程 / scan 进程内)
|
||||
# VCheck - execution + reply side of the check request channel (in the parent / scan process)
|
||||
#
|
||||
# 通道形态(依附 --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": "<原因>"}
|
||||
# Channel (attached to --need-confirm; present only while the watchdog runs):
|
||||
# * A caller writes one line to stdin: {"check": "<exe path>", "id": <uint64>}
|
||||
# * The watchdog parses it with the repo json library; a string "check" plus an integer "id" marks
|
||||
# a check request, forwarded byte-for-byte (plus '\n') to the IPC pipe; it is not a heartbeat.
|
||||
# * The parent polls that pipe without blocking (VGuard.PollIpc) and hands each line to this module.
|
||||
# * This module runs the check and writes a single-line JSON reply to stdout:
|
||||
# ok {"check_result": true, "id": <id>, "viper": {..}} (no check_msg)
|
||||
# fail {"check_result": false, "id": <id>, "check_msg": "<reason>"}
|
||||
# Serialized by the repo json library (compact single line, key order = insertion order).
|
||||
#
|
||||
# 回执由仓库 json 库序列化(json.object_set + json.write(pool, obj, False)):
|
||||
# 紧凑单行、转义与 UTF-8 透传由库保证、键序 = 插入序(见 includes/json/**)。
|
||||
# Check content (same rule as the scan candidate filter, single source):
|
||||
# 1. PE/.PRVERSN via VPe.TryReadPeInfo (open errors split by VPe.PeOpenErr: NOT_FOUND / NO_PERMISSION)
|
||||
# 2. --version via VProc.RunVersionProbe (timeout / launch error code / first 5 chars "Viper")
|
||||
#
|
||||
# 检查内容(与扫盘候选过滤逐条同口径,口径单点来源):
|
||||
# ① PE/.PRVERSN:VPe.TryReadPeInfo(打开失败的错误码经 VPe.PeOpenErr 区分 NOT_FOUND/NO_PERMISSION)
|
||||
# ② --version :VProc.RunVersionProbe(超时 / 拉起失败的错误码 / 前 5 字符为 "Viper")
|
||||
# Failure reasons are only the fixed ASCII strings below (case-sensitive): NOT_FOUND, NO_PERMISSION,
|
||||
# NOT_EXE, NO_PRVERSN, PRVERSN_ERR, PREFIX_ERR, "EXE_ERR,0x<HEX>" (from VProc.ProbeLaunchErr),
|
||||
# TIMEOUT, and CHANNEL_UNAVAILABLE (this module's buffers / reply pool unavailable, CheckInit failed;
|
||||
# see EmitReplyUnavailable). Exact conditions are documented on the CK_* constants.
|
||||
#
|
||||
# 失败原因**只允许**以下固定英文字符串(大小写严格):
|
||||
# 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 has priority over the scan: once a request is recognized this module runs the whole
|
||||
# "read .PRVERSN -> run --version -> reply" synchronously; the tree scan stays at its VScan poll
|
||||
# point until PollIpc returns. Single-threaded, so check and scan never interleave.
|
||||
#
|
||||
# 优先级:check 比普通扫盘高。识别到请求后本模块**同步**完成"读 .PRVERSN → 执行 --version →
|
||||
# 回执"整段,期间树扫描停在 VScan 的轮询点(PaceDir/PaceFile/探测前/轮首末)上不前进;
|
||||
# 处理完 PollIpc 才返回扫盘循环。单线程串行 ⇒ 不存在"check 与树扫描交错"的窗口。
|
||||
# Revalidation (VScan.RevalidateDiscovered) reuses VerifyExecutable (same rule); failures are removed.
|
||||
#
|
||||
# 每轮完整重校:VScan.RevalidateDiscovered 复用本模块的 VerifyExecutable(同一口径的
|
||||
# "文件存在 + PE/.PRVERSN 可解析 + --version 前 5 字符为 Viper"),不合格的从缓存剔除。
|
||||
# Memory ownership:
|
||||
# * Buffers and the reply pool are created once in CheckInit (_mbuddy field buffers + a stdlib
|
||||
# arena) and reused for the process lifetime; each reply resets Check_Pool first.
|
||||
# * ParseCheckLine allocates its parse tree in a per-call stdlib arena and frees it before
|
||||
# returning; it never aliases the cache table (same approach as VCache.ParseCacheText).
|
||||
#
|
||||
# 内存归属:
|
||||
# * 缓冲与回执工作池在 CheckInit 内一次性建立(_mbuddy 的字段缓冲 + stdlib 的池 arena),
|
||||
# 进程生命周期内复用;回执每条先 Check_Pool.reset()(池 offset 归零)再构造;
|
||||
# * ParseCheckLine 的解析树放在**本次调用专用**的 stdlib arena 上,返回前整块 free:
|
||||
# 与缓存表(_mbuddy 上 CacheAdd 的持久副本)互不相干,不会悬空/双重释放
|
||||
# (与 VCache.ParseCacheText 同一手法与理由)。
|
||||
# Degradation (fail-safe, never silently disabled): if CheckInit fails, HandleCheckLine still
|
||||
# recognizes the request and replies check_result=false + check_msg="CHANNEL_UNAVAILABLE" using a
|
||||
# stdlib temp buffer (independent of _mbuddy/Check_Pool); VerifyExecutable returns 1 (skip
|
||||
# revalidation, drop nothing) so a table is never wiped by an allocation failure.
|
||||
#
|
||||
# 降级(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。
|
||||
# Injection: _mbuddy must be the same MemBuddy instance injected by App/main.vp; otherwise all
|
||||
# allocations return None and CheckInit fails.
|
||||
# ============================================================
|
||||
|
||||
# 固定失败原因(check_msg 的全部取值;EXE_ERR 是前缀 + 十六进制码,见 FormatExeErr)
|
||||
# fixed failure reasons (all check_msg values; EXE_ERR is a prefix + hex code, see FormatExeErr)
|
||||
define CK_NOT_FOUND = "NOT_FOUND"
|
||||
define CK_NO_PERMISSION = "NO_PERMISSION"
|
||||
define CK_NOT_EXE = "NOT_EXE"
|
||||
define CK_NO_PRVERSN = "NO_PRVERSN"
|
||||
define CK_PRVERSN_ERR = "PRVERSN_ERR"
|
||||
define CK_PREFIX_ERR = "PREFIX_ERR" # --version 拉起成功但输出前 5 字符不是 Viper
|
||||
define CK_PREFIX_ERR = "PREFIX_ERR" # --version launched but the first 5 chars are not Viper
|
||||
define CK_TIMEOUT = "TIMEOUT"
|
||||
# 通道自身不可用(CheckInit 失败):降级回执的唯一原因串,见 EmitReplyUnavailable
|
||||
# channel itself unavailable (CheckInit failed): the only degradation reason, see EmitReplyUnavailable
|
||||
define CK_CHANNEL_UNAVAILABLE = "CHANNEL_UNAVAILABLE"
|
||||
|
||||
# check 命中入库时的 source 值(与扫盘侧 "workspace" 区分)
|
||||
# source value when a check hit is cached (distinct from the scan side's "workspace")
|
||||
define CK_SOURCE = "check"
|
||||
|
||||
# EXE_ERR 的原因串缓冲("EXE_ERR,0x" + 最多 8 位十六进制 + NUL)
|
||||
# EXE_ERR reason buffer ("EXE_ERR,0x" + up to 8 hex digits + NUL)
|
||||
define CK_MSG_CAP = 32
|
||||
|
||||
# 请求行解析上限(与 VGuard.GUARD_LINE_CAP 对齐:看门狗的行缓冲即本上限,
|
||||
# 超长行在看门狗侧已被截断/丢弃,这里再兜一层,避免对异常长的输入做解析)
|
||||
# Request-line parse limit, aligned with VGuard.GUARD_LINE_CAP (the watchdog already truncates or
|
||||
# drops overlong lines; this is a second guard against parsing an abnormally long input).
|
||||
define CK_LINE_MAX = 4096
|
||||
|
||||
# 解析工作池容量 / 输入字节与固定余量:与 VCache.ParseCacheText 同一保守口径
|
||||
# (最坏 ≈ 每输入字节 1 个 JsonValue 节点 + 键名/值副本 + 数字临时缓冲 ≈ 80 字节/输入字节,
|
||||
# 再按 128 放大覆盖 8 字节对齐浪费)。MemPool 耗尽会 exit(1),故必须先 AllocOk 预检。
|
||||
# Parse arena size per input byte + fixed slack, same conservative rule as VCache.ParseCacheText
|
||||
# (worst case ~1 JsonValue node + key/value copies + numeric temp per input byte; x128 covers
|
||||
# 8-byte alignment waste). MemPool exhaustion calls exit(1), so AllocOk is pre-checked.
|
||||
define CK_ARENA_PER_BYTE = 128
|
||||
define CK_ARENA_SLACK = 4096
|
||||
|
||||
# 回执工作池:72 KiB。单条回执的全部池分配(Need)见 CK_REPLY_NEED_FIXED 的推导。
|
||||
# Reply pool: 72 KiB. See CK_REPLY_NEED_FIXED for the per-reply allocation bound.
|
||||
define CK_REPLY_POOL_CAP = 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 对齐)。
|
||||
# Fixed upper bound per reply on the reply pool: 9 JsonValue nodes (648), key-name copies incl. NUL
|
||||
# (68), json.write output buffer (4096), integer serialization temp (32), plus 512 slack for NUL and
|
||||
# alignment waste. Total 5356 => round up to 5632 (512-aligned).
|
||||
define CK_REPLY_NEED_FIXED = 5632
|
||||
|
||||
# 回执里 executable / displayName 的取值长度上界:二者都经 Check_Conv(VFs.CONV_CAP 字节)
|
||||
# 转出(WideCharToMultiByte 以 Cap 封顶并写结尾 NUL,失败时 CopyRaw 写空串)⇒ ≤ CONV_CAP-1。
|
||||
# 写 2047 = VFs.CONV_CAP(2048) - 1 的字面量(与本仓"t.CDefine 不引用跨模块常量"的惯例一致)。
|
||||
# Upper bound for executable / displayName in a reply: both are converted through Check_Conv
|
||||
# (VFs.CONV_CAP bytes), so their length is <= CONV_CAP-1 = 2047 (literal, per repo convention).
|
||||
define CK_CONV_MAX = 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。
|
||||
# Temp line buffer size for the degradation reply (EmitReplyUnavailable): stdlib.malloc, not
|
||||
# _mbuddy (this path is exactly "mb buffers unavailable"). Upper bound incl. '\n' and NUL = 84 => 128.
|
||||
define CK_FALLBACK_CAP = 128
|
||||
|
||||
# 由 App/main.vp 注入(与其它模块同一 MemBuddy 实例)
|
||||
# injected by App/main.vp (same MemBuddy instance as other modules)
|
||||
_mbuddy: memhub.MemBuddy*
|
||||
|
||||
# 一次性初始化的状态(Check_Tried 保证"初始化失败"不会每次调用都重试一遍分配)
|
||||
# one-time init state (Check_Tried ensures a failed init is not retried on every call)
|
||||
Check_Tried: int = 0
|
||||
Check_Ready: int = 0
|
||||
Check_Handling: int = 0 # 1=正在处理一条 check(防重入;PollIpc 由扫盘多处调用)
|
||||
Check_Handling: int = 0 # 1 = handling a check (re-entrancy guard; PollIpc is called from several scan points)
|
||||
|
||||
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: memhub.MemPool* = None # 回执工作池(堆上就地构造,条目级 reset 复用)
|
||||
Check_Ver: bytes = None # .PRVERSN version string (VPe.VER_CAP)
|
||||
Check_Arch: bytes = None # arch string (VPe.ARCH_CAP)
|
||||
Check_Probe: bytes = None # --version capture buffer (VProc.VERSION_PROBE_DIAG_CAP)
|
||||
Check_Msg: bytes = None # EXE_ERR reason buffer (CK_MSG_CAP)
|
||||
Check_Path: bytes = None # exe path from the request (ANSI; VFs.PATH_BUF_CAP)
|
||||
Check_Id: bytes = None # UUID of a new entry (VGuid.GUID_STR_CAP)
|
||||
Check_Disp: bytes = None # "Viper " + version (VCache.DISP_CAP)
|
||||
Check_Conv: bytes = None # ANSI -> UTF-8 conversion buffer (VFs.CONV_CAP)
|
||||
Check_Arena: bytes = None # reply pool arena (stdlib.malloc, process-lifetime)
|
||||
Check_Pool: memhub.MemPool* = None # reply pool (constructed in place on the heap, reset per reply)
|
||||
|
||||
# ParseCheckLine 的结果(模块全局出参,与 VPe.Pe_RawPtr 同一惯例)
|
||||
Req_Id: LONGLONG = 0 # 请求里的 id(json 库 int64 域;回执原样带回)
|
||||
Req_PathOk: int = 0 # 1=Check_Path 已填(可执行检查);0=请求已识别但路径缓冲不可用
|
||||
# ParseCheckLine result (module globals, same convention as VPe.Pe_RawPtr)
|
||||
Req_Id: LONGLONG = 0 # request id (json int64 field; echoed back in the reply)
|
||||
Req_PathOk: int = 0 # 1 = Check_Path filled (check can run); 0 = request recognized but path buffer unavailable
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 初始化
|
||||
# initialization
|
||||
# ============================================================
|
||||
|
||||
# 请求路径缓冲(请求行解析与 CheckInit 共用)。看门狗进程只做"分类转发",不跑 CheckInit,
|
||||
# 但它也要解析请求行(取出路径)⇒ 该缓冲由最先用到的一方按需分配,两边都不重复分配。
|
||||
# Request path buffer (shared by line parsing and CheckInit). The watchdog process does not run
|
||||
# CheckInit but still parses request lines, so the buffer is allocated on first use by either side.
|
||||
def EnsurePathBuf() -> int:
|
||||
global Check_Path
|
||||
if Check_Path is None:
|
||||
@@ -168,11 +138,10 @@ def EnsurePathBuf() -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 分配检查缓冲与回执工作池(幂等;失败只告警一次并由 Check_Ready 标记为不可用)。
|
||||
# 失败语义(fail-safe):check 请求回执 CHANNEL_UNAVAILABLE(见 EmitReplyUnavailable);
|
||||
# 每轮重校直接跳过(不剔除任何条目)——绝不因"缓冲分配失败"而把整张缓存表误判为不合格清掉。
|
||||
# 告警按"哪一组分配失败"区分:四组缓冲来源不同(_mbuddy 上的字段缓冲 / stdlib 的池 arena),
|
||||
# 同一句文案会让人无法判断是哪一处(历史现场:_mbuddy 未注入时四组全失败,只能看到同一句)。
|
||||
# Allocate check buffers and the reply pool (idempotent; a failure warns once and marks Check_Ready
|
||||
# unavailable). Fail-safe: check requests reply CHANNEL_UNAVAILABLE and revalidation is skipped
|
||||
# (nothing is dropped) - never wipe the cache table due to an allocation failure. Warnings are
|
||||
# grouped so the failing allocation set is identifiable.
|
||||
def CheckInit() -> int:
|
||||
global Check_Tried, Check_Ready
|
||||
global Check_Ver, Check_Arch, Check_Probe, Check_Msg
|
||||
@@ -200,8 +169,8 @@ def CheckInit() -> int:
|
||||
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)。
|
||||
# Construct the pool in place on the heap (memhub.NewMemPool): Check_Pool outlives this frame
|
||||
# (same reason as VJson.JsonInit).
|
||||
Pool: memhub.MemPool* = memhub.NewMemPool(Check_Arena, CK_REPLY_POOL_CAP)
|
||||
if Pool is None:
|
||||
VLog.LogStr(" 警告: check 回执工作池构造失败,check 通道不可用\n")
|
||||
@@ -212,21 +181,17 @@ def CheckInit() -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 请求行解析(看门狗分类与父进程执行共用同一口径)
|
||||
# request-line parsing (shared rule for the watchdog classification and the parent execution)
|
||||
# ============================================================
|
||||
|
||||
# 用仓库 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 同一只读遍历写法),两个键各自记"是否命中",
|
||||
# 最后要求两者都命中——不依赖键的先后顺序,也不做任何字符串搜索。
|
||||
# Parse one text line with the repo json library and decide whether it is a check request.
|
||||
# Returns 1 = yes (Req_Id set; when Req_PathOk=1, Check_Path holds the ANSI path), 0 = no.
|
||||
# Rule (no string guessing): a top-level JSON object with a non-empty string "check" and an integer
|
||||
# "id"; anything else (non-object, missing keys, wrong type, empty path, non-JSON, empty line) => 0.
|
||||
# "id" must be numeric ("id":"1" is a string and does not count). Req_PathOk=0 means "request valid
|
||||
# but path buffer unavailable"; still returns 1 so the caller replies CHANNEL_UNAVAILABLE and the
|
||||
# watchdog forwards it, never mistaking a check request for heartbeat input. Single pass over the
|
||||
# top-level children; key order does not matter.
|
||||
def ParseCheckLine(Txt: str) -> int:
|
||||
global Req_Id, Req_PathOk
|
||||
Req_Id = 0
|
||||
@@ -244,9 +209,9 @@ def ParseCheckLine(Txt: str) -> int:
|
||||
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) 恒成立;此处只是把"容量推导被改小"变成可控降级。
|
||||
# Pool capacity pre-check (fail-closed): the parse tree must be known to fit, otherwise
|
||||
# json.parse's pool.alloc reaches printf + exit(1). Need and Cap share the same derivation, so
|
||||
# AllocOk(Cap) always holds; this only turns a shrunk derivation into a controlled degradation.
|
||||
if Pool.AllocOk(Cap) == 0:
|
||||
stdlib.free(t.CPtr(Arena))
|
||||
return 0
|
||||
@@ -261,9 +226,9 @@ def ParseCheckLine(Txt: str) -> int:
|
||||
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 请求"。
|
||||
# Console/command-line input arrives as UTF-8; the A-series API needs ANSI.
|
||||
# Utf8ToAnsi copies invalid UTF-8 as-is, so plain ANSI input also works.
|
||||
# If the path buffer is unavailable, Req_PathOk stays 0 but it is still a check.
|
||||
if EnsurePathBuf() != 0:
|
||||
VFs.Utf8ToAnsi(cu.str_val, Check_Path, VFs.PATH_BUF_CAP)
|
||||
Req_PathOk = 1
|
||||
@@ -278,27 +243,18 @@ def ParseCheckLine(Txt: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# ParseCheckLine 的解析结果经模块全局出参暴露:Check_Path(ANSI 路径)、Req_Id(请求 id)
|
||||
# 与 Req_PathOk(路径是否可用),与 VPe.Pe_RawPtr / VFs.LastReadSize 同一惯例
|
||||
# (同模块内直接读,跨模块只走函数)。
|
||||
# ParseCheckLine exposes its result through module globals: Check_Path (ANSI path), Req_Id and
|
||||
# Req_PathOk (same convention as VPe.Pe_RawPtr / VFs.LastReadSize; read directly within the module).
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 失败原因映射
|
||||
# failure reason mapping
|
||||
# ============================================================
|
||||
|
||||
# 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
|
||||
# Map a PE parse failure code (VPe.TryReadPeInfo rc + ParsePeHead prc) to a fixed reason string.
|
||||
# rc 1 (CreateFileA failed) splits by OpenErr: 2/3 = NOT_FOUND, else NO_PERMISSION. rc 2..5 => NOT_EXE;
|
||||
# rc 6/7 => PRVERSN_ERR; rc 5/107 => NO_PRVERSN. prc (rc-100): 7 = NO_PRVERSN, 6 = PRVERSN_ERR,
|
||||
# others = NOT_EXE.
|
||||
def MapPeFailure(Rc: int, OpenErr: ULONG) -> str:
|
||||
if Rc == 1:
|
||||
if OpenErr == 2 or OpenErr == 3:
|
||||
@@ -318,54 +274,49 @@ def MapPeFailure(Rc: int, OpenErr: ULONG) -> str:
|
||||
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 会把它复制进回执池)。
|
||||
# Format a launch-failure code as `EXE_ERR,0x<UPPERHEX>` (writes Check_Msg, returns it), e.g.
|
||||
# 2 -> "EXE_ERR,0x2", 193 -> "EXE_ERR,0xC1". The result points to a module buffer and must be used
|
||||
# immediately (EmitReplyFail copies it into the reply pool).
|
||||
def FormatExeErr(Err: ULONG) -> str:
|
||||
viperlib.snprintf(Check_Msg, sizet(CK_MSG_CAP), "EXE_ERR,0x%X", Err)
|
||||
return str(Check_Msg)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 检查执行
|
||||
# check execution
|
||||
# ============================================================
|
||||
|
||||
# 对一个 exe 做完整检查(PE/.PRVERSN + --version)。
|
||||
# 成功:返回 None,并把版本串/arch 写入 Check_Ver / Check_Arch、把 "Viper "+版本串
|
||||
# 写入 Check_Disp(供调用方构造回执与入库)。
|
||||
# 失败:返回固定的原因串(CK_* 常量或 FormatExeErr 的共享缓冲)。
|
||||
# 说明:本函数**不做缓存写回**,只做"检查 + 填缓冲";入库与"已存在则不重写"由调用方决定。
|
||||
# Full check of one exe (PE/.PRVERSN + --version). On success returns None and fills Check_Ver /
|
||||
# Check_Arch and Check_Disp ("Viper " + version); on failure returns a fixed reason string (a CK_*
|
||||
# constant or FormatExeErr's shared buffer). This function does no cache write-back.
|
||||
def CheckExecutable(PathAnsi: str) -> str:
|
||||
Check_Ver[0] = 0
|
||||
Check_Arch[0] = 0
|
||||
# ① PE / .PRVERSN(版本号与 arch 的静态来源)
|
||||
# (1) PE / .PRVERSN (static source of version and 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 才算通过)
|
||||
return CK_PRVERSN_ERR # section present but empty (first byte is NUL)
|
||||
# (2) --version probe passes only if the process ends normally and the first 5 chars are "Viper"
|
||||
pc: int = VProc.RunVersionProbe(PathAnsi, Check_Probe,
|
||||
sizet(VProc.VERSION_PROBE_DIAG_CAP), 0)
|
||||
if pc == 2 or VProc.Probe_TimedOut != 0:
|
||||
return CK_TIMEOUT
|
||||
if pc != 0:
|
||||
# 拉起失败(参数/命令缓冲/管道/CreateProcessA):统一报 EXE_ERR,0x<码>
|
||||
# launch failed (args/command buffer/pipe/CreateProcessA): report EXE_ERR,0x<code>
|
||||
return FormatExeErr(VProc.ProbeLaunchErr())
|
||||
if VProc.OutputHasViperPrefix(str(Check_Probe)) == 0:
|
||||
# 拉起成功但输出前 5 字符不是 Viper ⇒ 独立原因 PREFIX_ERR(与段定位/读取/内容异常区分)
|
||||
# launched but the first 5 chars are not Viper => distinct reason PREFIX_ERR
|
||||
return CK_PREFIX_ERR
|
||||
viperlib.snprintf(Check_Disp, sizet(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 通道缓冲不可用"的轮级告警,两者共同说明"本轮不重校"的原因。
|
||||
# Full revalidation of a discovered item: file exists + PE/.PRVERSN parseable + --version first 5
|
||||
# chars are Viper (all CheckExecutable conditions, without needing version/arch). Returns 1 = keep,
|
||||
# 0 = drop. Fail-safe: if CheckInit failed, returns 1 (drop nothing) so the table is never wiped;
|
||||
# the warning is emitted once by CheckInit (plus a scan-start note from VScan.ScanInit).
|
||||
def VerifyExecutable(PathAnsi: str) -> int:
|
||||
if CheckInit() == 0:
|
||||
return 1
|
||||
@@ -375,13 +326,12 @@ def VerifyExecutable(PathAnsi: str) -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 入库(check 命中)
|
||||
# cache insert (check hit)
|
||||
# ============================================================
|
||||
|
||||
# 把 check 命中项写入缓存(**已存在则不重写**:不改其 id/字段,直接跳过)。
|
||||
# 返回 1 = 命中项已在缓存中(无论本轮是否新写入);0 = 缓存不可用。
|
||||
# 幂等性:CacheFind 按 ANSI 路径精确匹配(与扫盘 HandleCandidate 同一判定),
|
||||
# 因此同一 exe 反复 check 不会重复入表,也不会刷新既有 id/version/arch/source。
|
||||
# Insert a check hit into the cache (if it already exists, skip without rewriting its id/fields).
|
||||
# Returns 1 = the hit is in the cache (newly written or already present), 0 = cache unavailable.
|
||||
# Idempotent: CacheFind matches by exact ANSI path, so repeated checks do not reinsert or refresh.
|
||||
def TrackChecked(PathAnsi: str) -> int:
|
||||
if VCache.CacheFind(PathAnsi) >= 0:
|
||||
return 1
|
||||
@@ -393,13 +343,12 @@ def TrackChecked(PathAnsi: str) -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 回执(stdout 单行 JSON)
|
||||
# reply (single-line JSON on stdout)
|
||||
# ============================================================
|
||||
|
||||
# 写一行 stdout(自带结尾 '\n',且**一次 WriteFile 写完**):
|
||||
# json.write 的输出缓冲容量是 4096(缺省 cap),本文本远小于它,故 NUL 之后仍有空位放
|
||||
# 换行与新的 NUL。这样做是为了让"一行回执"始终是**单次写**——与别人(看门狗失败行、
|
||||
# 下一轮数组)写入不会在同一行中间交错。
|
||||
# Write one stdout line (with trailing '\n', in a single WriteFile). json.write's output buffer is
|
||||
# 4096, far larger than this text, so there is room after the NUL for '\n' and a new NUL; a single
|
||||
# write keeps a reply line from interleaving with other writers.
|
||||
def WriteReplyLine(Txt: str) -> int:
|
||||
n: sizet = string.strlen(Txt)
|
||||
if n + 2 <= sizet(4096):
|
||||
@@ -407,16 +356,15 @@ def WriteReplyLine(Txt: str) -> int:
|
||||
Txt[n + 1] = 0
|
||||
VLog.StdoutWrite(Txt)
|
||||
return 1
|
||||
# 不可达(长度守卫保证文本远小于 4096):仅为不写出无换行的残行而保留的后备分支
|
||||
# unreachable (the length guard keeps the text far below 4096): fallback to avoid a newline-less line
|
||||
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 稳定。
|
||||
# Success reply: {"check_result": true, "id": <id>, "viper": {<found item>}}. The viper dict has the
|
||||
# same shape/order as existing found items (id/executable/version/arch/source/displayName); source
|
||||
# is "check". A cached exe reuses its cache fields (id stable, nothing rewritten).
|
||||
def EmitReplyOk(ReqIdVal: LONGLONG, PathAnsi: str):
|
||||
Id: str = str(Check_Id)
|
||||
Ver: str = str(Check_Ver)
|
||||
@@ -424,13 +372,13 @@ def EmitReplyOk(ReqIdVal: LONGLONG, PathAnsi: str):
|
||||
Disp: str = str(Check_Disp)
|
||||
idx: int = VCache.CacheFind(PathAnsi)
|
||||
if idx >= 0:
|
||||
# 已在缓存中:不改其 id/字段("已存在则不写"),回执用缓存里的记录(id 稳定)
|
||||
# already cached: do not change its id/fields; reply with the cached record (stable id)
|
||||
Id = VCache.CacheId(idx)
|
||||
Ver = VCache.CacheVer(idx)
|
||||
Arch = VCache.CacheArch(idx)
|
||||
Disp = VCache.CacheDisp(idx)
|
||||
else:
|
||||
# 新命中:生成 UUID 后入库(失败仅告警:回执的 id 为空串,仍如实答复检查结果)
|
||||
# new hit: generate a UUID then insert (failure only warns; the reply still reports the result)
|
||||
if VGuid.NewGuidStr(Check_Id, VGuid.GUID_STR_CAP) == 0:
|
||||
VLog.LogStr(" 警告: CoCreateGuid 失败,check 入库项的 id 为空串\n")
|
||||
Check_Id[0] = 0
|
||||
@@ -454,7 +402,7 @@ def EmitReplyOk(ReqIdVal: LONGLONG, PathAnsi: str):
|
||||
VLog.LogStr(" 警告: check 回执对象分配失败,未回复\n")
|
||||
return
|
||||
json.object_set(Check_Pool, Sub, "id", json.string_val(Check_Pool, Id))
|
||||
# executable / displayName 转 UTF-8 后**立即**交给库复制(Check_Conv 随后要复用)
|
||||
# convert executable / displayName to UTF-8 and hand to the library immediately (Check_Conv is reused)
|
||||
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)))
|
||||
@@ -472,7 +420,7 @@ def EmitReplyOk(ReqIdVal: LONGLONG, PathAnsi: str):
|
||||
WriteReplyLine(Txt)
|
||||
|
||||
|
||||
# 失败回执:{"check_result": false, "id": <id>, "check_msg": "<原因>"}
|
||||
# Failure reply: {"check_result": false, "id": <id>, "check_msg": "<reason>"}
|
||||
def EmitReplyFail(ReqIdVal: LONGLONG, Msg: str):
|
||||
Check_Pool.reset()
|
||||
Need: sizet = sizet(CK_REPLY_NEED_FIXED)
|
||||
@@ -495,13 +443,12 @@ def EmitReplyFail(ReqIdVal: LONGLONG, Msg: str):
|
||||
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=连临时缓冲都拿不到(此时确实无法回复,只能不回复)。
|
||||
# Degradation reply (compact single line): {"check_result": false, "id": <id>,
|
||||
# "check_msg": "CHANNEL_UNAVAILABLE"}. Used only when CheckInit failed, so it must not depend on
|
||||
# _mbuddy / Check_Pool: the line buffer comes from stdlib.malloc (freed immediately), and the body
|
||||
# is produced by viperlib.snprintf with a fixed template matching the library's compact form (no
|
||||
# escaping needed). This keeps "request received but channel unavailable" visible. Returns 1 =
|
||||
# replied, 0 = even the temp buffer is unavailable.
|
||||
def EmitReplyUnavailable(ReqIdVal: LONGLONG) -> int:
|
||||
Fb: bytes = stdlib.malloc(CK_FALLBACK_CAP)
|
||||
if Fb is None:
|
||||
@@ -515,15 +462,13 @@ def EmitReplyUnavailable(ReqIdVal: LONGLONG) -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 一行请求的完整处理(由 VGuard.PollIpc 在非阻塞轮询点调用)
|
||||
# full handling of one request line (called by VGuard.PollIpc at a non-blocking poll point)
|
||||
# ============================================================
|
||||
|
||||
# 执行一条 check 请求行:解析 → 检查 → 回执。返回 1=已处理(无论成功/失败都已回执),
|
||||
# 0=不是 check 请求/未处理。
|
||||
# 防重入:Check_Handling 为 1 时直接返回 0——扫描侧的轮询点是"高频调用",但 check 的处理
|
||||
# 不会从 check 内部再次发起轮询;该标记只是把"未来若在检查路径上新增轮询"这类改动挡住。
|
||||
# 通道降级:CheckInit 失败(缓冲/回执池不可用)时不再静默丢弃,而是先识别请求再回执
|
||||
# CHANNEL_UNAVAILABLE(EmitReplyUnavailable),使"请求已收到、但无法执行检查"可被请求方区分。
|
||||
# Handle one check request line: parse -> check -> reply. Returns 1 = handled (replied regardless
|
||||
# of success/failure), 0 = not a check request / not handled. Re-entrancy: returns 0 while
|
||||
# Check_Handling is 1. Degradation: if CheckInit failed, still recognize the request and reply
|
||||
# CHANNEL_UNAVAILABLE rather than silently dropping it.
|
||||
def HandleCheckLine(Txt: str) -> int:
|
||||
global Check_Handling
|
||||
if Check_Handling != 0:
|
||||
|
||||
+73
-147
@@ -6,97 +6,59 @@ 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)。
|
||||
# ============================================================
|
||||
# VCli - command-line parsing for scan/ver (hand-written, no argparse).
|
||||
# Exposes parsed flags via accessors consumed by VScan/VPe/App.main.vp.
|
||||
|
||||
define VET_VERSION = "0.2.0"
|
||||
define SECS_MAX = 86400 # --need-confirm 秒数上界(1 天;超出视为参数错误)
|
||||
define DEFAULT_SLEEP_MS = 10000 # --sleep 缺省值(沿用原 SCAN_INTERVAL_MS)
|
||||
define SLEEP_MAX_MS = 86400000 # --sleep 毫秒上界(1 天;超出视为参数错误)
|
||||
define DEFAULT_CONFIRM_TIMEOUT_MS = 5000 # --need-confirm-timeout 缺省值
|
||||
define CONFIRM_TIMEOUT_MAX_MS = 86400000 # --need-confirm-timeout 毫秒上界(1 天)
|
||||
# --pace <毫秒>:节流点睡眠时长,取值域 0..60000。0 = 关闭节流(两个节流点都不 Sleep)。
|
||||
# 上界取 1 分钟:节流只为降低扫盘对机器的持续压力,睡眠远超 1 分钟只会无谓拖慢单轮而无收益,
|
||||
# 与 --sleep(轮间隔,上界 1 天)的用途不同,故不共用其宽松上界。
|
||||
define DEFAULT_PACE_SLEEP_MS = 20 # --pace 缺省值(沿用 VScan.PACE_SLEEP_MS,未给出时逐字保持现状)
|
||||
define PACE_SLEEP_MAX_MS = 60000 # --pace 毫秒上界(1 分钟)
|
||||
# --pace-units <N>:文件粒度,取值域 1..1000000(每 N 个文件一个节流点;1 = 每个文件都停)。
|
||||
# 0 无意义("每 0 个文件"),与非法输入一并报错;上界 1000000 已足够稀疏(等同关闭文件侧节流)。
|
||||
define DEFAULT_PACE_FILE_UNITS = 64 # --pace-units 缺省值(沿用 VScan.PACE_FILE_UNITS,未给出时逐字保持现状)
|
||||
define PACE_UNITS_MAX = 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 全域)
|
||||
define SECS_MAX = 86400 # --need-confirm seconds upper bound (1 day; over = argument error)
|
||||
define DEFAULT_SLEEP_MS = 10000 # --sleep default (ms)
|
||||
define SLEEP_MAX_MS = 86400000 # --sleep ms upper bound (1 day; over = argument error)
|
||||
define DEFAULT_CONFIRM_TIMEOUT_MS = 5000 # --need-confirm-timeout default (ms)
|
||||
define CONFIRM_TIMEOUT_MAX_MS = 86400000 # --need-confirm-timeout ms upper bound (1 day)
|
||||
# --pace <ms>: throttle sleep duration, range 0..60000; 0 = disable throttling (no Sleep).
|
||||
# Upper bound is 1 minute (its purpose differs from --sleep's 1-day bound).
|
||||
define DEFAULT_PACE_SLEEP_MS = 20 # --pace default (ms)
|
||||
define PACE_SLEEP_MAX_MS = 60000 # --pace ms upper bound (1 minute)
|
||||
# --pace-units <N>: file granularity, range 1..1000000 (a sleep point every N files; 1 = every file).
|
||||
# 0 is meaningless and rejected along with invalid input; upper bound 1000000 is sparse enough.
|
||||
define DEFAULT_PACE_FILE_UNITS = 64 # --pace-units default
|
||||
define PACE_UNITS_MAX = 1000000 # --pace-units file-count upper bound
|
||||
# --parent-pid upper bound (full DWORD range 4294967295).
|
||||
# Use a wide global (LONGLONG): a t.CDefine in u32 range materializes as i32 and truncates.
|
||||
PID_MAX: LONGLONG = 4294967295 # --parent-pid upper bound (full DWORD range)
|
||||
|
||||
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"它扫不到;用本标记在解析后重新并入,两种写法语义完全一致。
|
||||
Verbose_Flag: int = 0 # --verbose (scan); 1 = verbose logging
|
||||
# Set by App/main.vp for the top-level form (VET.exe --search-paths ...): ParseScanArgs scans
|
||||
# from argv[2], missing the switch at the subcommand slot; merged back as a flag after parsing.
|
||||
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=第一轮不启用节流
|
||||
Search_Paths_Flag: int = 0 # --search-paths (scan); 1 = print searchable paths and exit
|
||||
# --pace/--pace-units (scan): explicitly given => enable throttling (also under --once).
|
||||
# Pace_Set records whether either was given; otherwise VScan keeps its own defaults.
|
||||
Pace_Set: int = 0 # 1 = --pace or --pace-units given on the command line
|
||||
Pace_Ms: int = DEFAULT_PACE_SLEEP_MS # --pace <ms> (0 = disable throttling)
|
||||
Pace_Units: int = DEFAULT_PACE_FILE_UNITS # --pace-units <N> (sleep point every N files; 1 = every file)
|
||||
# --first-no-pace (scan): disable throttling in round 1 only; later rounds follow normal rules.
|
||||
# Does not change Pace_Set/Pace_Ms/Pace_Units; distinct from --pace 0 (no throttle in any round).
|
||||
First_No_Pace_Flag: int = 0 # 1 = round 1 runs without throttling
|
||||
|
||||
# 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 时追加传入)
|
||||
# confirm-watch (hidden internal subcommand) params
|
||||
Watch_Interval: int = 0 # seconds (passed by parent via --need-confirm)
|
||||
Watch_Timeout: int = -1 # ms; -1 = not given (later set to DEFAULT_CONFIRM_TIMEOUT_MS)
|
||||
Watch_ParentPid: LONGLONG = 0 # parent pid (wide type: carries full DWORD, see PID_MAX)
|
||||
Watch_IpcWrite: ULONGLONG = 0 # check-channel pipe write-end handle value (0 = not given => no channel)
|
||||
Watch_Verbose: int = 0 # --verbose (appended by the parent when verbose)
|
||||
|
||||
# ParseUint64 的出参(模块全局,与 VPe.Pe_RawPtr / VFs.LastReadSize 同一惯例)
|
||||
# ParseUint64 output (module global, same convention as VPe.Pe_RawPtr)
|
||||
U64_Val: ULONGLONG = 0
|
||||
|
||||
|
||||
# 版本号经函数暴露给 VPe/VScan(跨模块"函数调用"路径明确支持;
|
||||
# 避免依赖跨模块字符串 t.CDefine 取值——本仓已证实的是跨模块整型 t.CDefine)
|
||||
# Version exposed via a function to VPe/VScan (cross-module calls are supported; avoids relying
|
||||
# on a cross-module string t.CDefine).
|
||||
def Version() -> str:
|
||||
return VET_VERSION
|
||||
|
||||
@@ -129,8 +91,8 @@ def SearchPaths() -> int:
|
||||
return Search_Paths_Flag
|
||||
|
||||
|
||||
# --pace/--pace-units 取值:PaceSet()==0 时调用方(VScan.RunScan)应完全忽略后两者、沿用自身缺省,
|
||||
# 以保证"未给出参数时行为逐字不变";==1 时三者的组合语义见 RunScan 的注释。
|
||||
# --pace/--pace-units values: when PaceSet()==0 the caller (VScan.RunScan) ignores the other two
|
||||
# and keeps its own defaults; when ==1 see RunScan for the combined semantics.
|
||||
def PaceSet() -> int:
|
||||
return Pace_Set
|
||||
|
||||
@@ -143,14 +105,13 @@ def PaceFileUnits() -> int:
|
||||
return Pace_Units
|
||||
|
||||
|
||||
# --first-no-pace:是否要求"第 1 轮不启用节流"。1=是(仅第 1 轮;之后各轮按常规规则)。
|
||||
# --first-no-pace: 1 = round 1 runs without throttling (later rounds follow normal rules).
|
||||
def FirstNoPace() -> int:
|
||||
return First_No_Pace_Flag
|
||||
|
||||
|
||||
# 顶层写法专用:`VET.exe --search-paths ...`(子命令位置就是该开关)。
|
||||
# 必须在 ParseScanArgs 之前调用——它把该意图记进 Search_Paths_Implicit,解析结束时再并入
|
||||
# Search_Paths_Flag;这样解析里"Search_Paths_Flag == 0 才要求 --cache"的放宽也一并生效。
|
||||
# For the top-level form (VET.exe --search-paths ...); call before ParseScanArgs. Records the
|
||||
# intent in Search_Paths_Implicit, merged into Search_Paths_Flag after parsing.
|
||||
def SetSearchPathsImplicit():
|
||||
global Search_Paths_Implicit
|
||||
Search_Paths_Implicit = 1
|
||||
@@ -176,8 +137,8 @@ def WatchVerbose() -> int:
|
||||
return Watch_Verbose
|
||||
|
||||
|
||||
# ver 子命令专用:ver 不解析开关(目标路径固定在 sys._argv[2]),
|
||||
# 只要 argv 中出现 --verbose 即视为要求详细日志(argv 起点与 scan 一致,从下标 2 扫)。
|
||||
# ver subcommand only: ver parses no switches (target is sys._argv[2]); --verbose anywhere in
|
||||
# argv means verbose (scanned from index 2 like scan).
|
||||
def HasVerbose() -> int:
|
||||
for i in range(2, sys._argc):
|
||||
A: str = sys._argv[i]
|
||||
@@ -187,28 +148,8 @@ def HasVerbose() -> int:
|
||||
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 位
|
||||
# 回绕(防御性,按当前上界不可达)。
|
||||
# Strictly parse a non-negative decimal integer: only '0'-'9' (empty/whitespace/sign/dot/others
|
||||
# => -1). Max is LONGLONG (signed 64-bit) so values above i32 range do not truncate.
|
||||
def ParseUint(S: str, Max: LONGLONG) -> LONGLONG:
|
||||
if S is None:
|
||||
return -1
|
||||
@@ -221,29 +162,22 @@ def ParseUint(S: str, Max: LONGLONG) -> LONGLONG:
|
||||
ch: int = int(ub[i])
|
||||
if ch < 48 or ch > 57:
|
||||
return -1
|
||||
if v > Max: # 上界短路:兼作 v*10 的溢出守卫(不变量兜底)
|
||||
if v > Max: # bounds short-circuit; also guards v*10 overflow
|
||||
return -1
|
||||
v = v * 10 + LONGLONG(ch - 48)
|
||||
if v > Max or v < 0: # 越界 / 64 位回绕(后者理论不可达)
|
||||
if v > Max or v < 0: # out of range / 64-bit wrap (latter unreachable)
|
||||
return -1
|
||||
i += 1
|
||||
return v
|
||||
|
||||
|
||||
# --need-confirm 的秒数解析:等价 ParseUint(S, SECS_MAX)(保持原判定口径与错误码 -1)
|
||||
# --need-confirm seconds parse: equivalent to ParseUint(S, SECS_MAX), keeps error code -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。
|
||||
# Strictly parse a non-negative decimal u64: only '0'-'9'; success returns 1 and stores U64_Val,
|
||||
# failure returns 0. Parses the full u64 range because --ipc-write carries a sign-extended handle.
|
||||
def ParseUint64(S: str) -> int:
|
||||
global U64_Val
|
||||
U64_Val = 0
|
||||
@@ -259,7 +193,7 @@ def ParseUint64(S: str) -> int:
|
||||
if ch < 48 or ch > 57:
|
||||
return 0
|
||||
d: ULONGLONG = ULONGLONG(ch - 48)
|
||||
if v > ULONGLONG(1844674407370955161): # 再乘 10 必溢出
|
||||
if v > ULONGLONG(1844674407370955161): # times 10 would overflow
|
||||
return 0
|
||||
if v == ULONGLONG(1844674407370955161) and d > ULONGLONG(5):
|
||||
return 0
|
||||
@@ -302,9 +236,8 @@ def PrintUsage():
|
||||
VLog.LogStr(" 不拉起看门狗、不读写缓存;此时 --cache 非必填。\n")
|
||||
|
||||
|
||||
# 解析 scan 参数。返回 0=成功,1=参数错误(已打印用法/错误)
|
||||
# 说明:本轮起 ParseScanArgs 由 App/main.vp 在分派 scan 时调用(看门狗生命周期需要
|
||||
# 在 RunScan 之前拿到 NeedConfirm),VScan.RunScan 不再自行解析。
|
||||
# Parse scan arguments; returns 0 = success, 1 = argument error (usage/error already printed).
|
||||
# Called by App/main.vp when dispatching scan (it needs NeedConfirm before 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
|
||||
@@ -320,8 +253,8 @@ def ParseScanArgs() -> int:
|
||||
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 预先置位,这里并入(见其声明处)。
|
||||
# Top-level form (VET.exe --search-paths ...): the switch sits at argv[1] (subcommand
|
||||
# slot), which this function skips; main.vp pre-sets it and it is merged in here.
|
||||
Search_Paths_Flag = 1
|
||||
Has_Timeout: int = 0
|
||||
i: int = 2
|
||||
@@ -338,9 +271,9 @@ def ParseScanArgs() -> int:
|
||||
elif string.strcmp(A, "--once") == 0:
|
||||
Once_Flag = 1
|
||||
elif string.strcmp(A, "--verbose") == 0:
|
||||
Verbose_Flag = 1 # 无值开关:只改 stderr 详细度
|
||||
Verbose_Flag = 1 # valueless switch: changes stderr verbosity only
|
||||
elif string.strcmp(A, "--search-paths") == 0:
|
||||
Search_Paths_Flag = 1 # 无值开关:查询"会搜索的路径"后直接退出(不扫盘/不碰缓存)
|
||||
Search_Paths_Flag = 1 # valueless switch: print searchable paths and exit (no scan, no cache)
|
||||
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)
|
||||
@@ -366,7 +299,7 @@ def ParseScanArgs() -> int:
|
||||
PrintUsage()
|
||||
return 1
|
||||
Pace_Ms = P
|
||||
Pace_Set = 1 # 显式给出 ⇒ 节流启用(--once 下也生效)
|
||||
Pace_Set = 1 # explicitly given => throttling on (also under --once)
|
||||
i += 1
|
||||
else:
|
||||
VLog.LogStr("错误: --pace 需要一个毫秒数参数\n")
|
||||
@@ -375,7 +308,7 @@ def ParseScanArgs() -> int:
|
||||
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 个文件一个节流点"无意义
|
||||
# 0 and invalid are both errors; 0 makes "a sleep point every 0 files" meaningless
|
||||
if U <= 0:
|
||||
VLog.LogFmt1("错误: --pace-units 需要一个 1..%u 的整数字面量文件数,实际为: ",
|
||||
sizet(PACE_UNITS_MAX))
|
||||
@@ -383,21 +316,21 @@ def ParseScanArgs() -> int:
|
||||
PrintUsage()
|
||||
return 1
|
||||
Pace_Units = U
|
||||
Pace_Set = 1 # 显式给出 ⇒ 节流启用(--once 下也生效)
|
||||
Pace_Set = 1 # explicitly given => throttling on (also under --once)
|
||||
i += 1
|
||||
else:
|
||||
VLog.LogStr("错误: --pace-units 需要一个文件数参数\n")
|
||||
PrintUsage()
|
||||
return 1
|
||||
elif string.strcmp(A, "--first-no-pace") == 0:
|
||||
# 无值开关:只要求"第 1 轮不节流",不改变节流参数本身、也不置 Pace_Set
|
||||
# (第 2 轮起的节流规则与未给该开关时逐字相同)。
|
||||
# Valueless switch: requests round-1 no-throttle only; it neither changes throttle
|
||||
# params nor sets Pace_Set (later rounds behave as if it were absent).
|
||||
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 只带字符串)
|
||||
# two fixed-arity calls on one line (LogFmt1 takes ints, LogFmtS1 takes strings)
|
||||
VLog.LogFmt1("错误: --need-confirm 需要一个 0..%u 的整数字面量秒数,实际为: ",
|
||||
sizet(SECS_MAX))
|
||||
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
|
||||
@@ -438,28 +371,21 @@ def ParseScanArgs() -> int:
|
||||
PrintUsage()
|
||||
return 1
|
||||
i += 1
|
||||
# --search-paths 是纯查询开关(只建表 + 打印,不读写缓存、不扫盘):未给 --cache 时放行。
|
||||
# 其余校验一律不放宽——给了 --cache 仍按原样接受(该查询只是不使用它)。
|
||||
# --search-paths is a pure query switch (build table + print, no cache, no scan), so --cache
|
||||
# is optional; other checks stay strict (a provided --cache is still accepted, just unused).
|
||||
if Cache_Path is None and Search_Paths_Flag == 0:
|
||||
VLog.LogStr("错误: scan 模式必须提供 --cache <path>\n")
|
||||
PrintUsage()
|
||||
return 1
|
||||
# 取舍:--need-confirm-timeout 单独出现时不报错(仅告警忽略)——它只是给
|
||||
# --need-confirm 的从属参数,报错会因标志顺序/复用脚本而无谓失败;告警避免静默无效。
|
||||
# --need-confirm-timeout alone is not an error, only a warning: it is subordinate to
|
||||
# --need-confirm, so erroring would fail needlessly on flag order; the warning avoids silence.
|
||||
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)自动拉起。
|
||||
# ============================================================
|
||||
# confirm-watch (hidden internal subcommand) argument parsing: e.g. VET.exe confirm-watch
|
||||
# --interval <s> --timeout <ms> --parent-pid <pid> --ipc-write <handle> [--verbose]; not in usage.
|
||||
|
||||
def ParseWatchArgs() -> int:
|
||||
global Watch_Interval, Watch_Timeout, Watch_ParentPid, Watch_IpcWrite, Watch_Verbose
|
||||
@@ -520,7 +446,7 @@ def ParseWatchArgs() -> int:
|
||||
VLog.LogStr("confirm-watch: --ipc-write 需要一个句柄数值参数\n")
|
||||
return 1
|
||||
elif string.strcmp(A, "--verbose") == 0:
|
||||
Watch_Verbose = 1 # 无值开关:由父进程按自身 verbose 追加
|
||||
Watch_Verbose = 1 # valueless switch: appended by the parent when it is verbose
|
||||
else:
|
||||
VLog.LogFmtS1("confirm-watch: 未知参数 %s\n", A)
|
||||
return 1
|
||||
|
||||
+59
-81
@@ -9,35 +9,21 @@ import w32.win32base as wb
|
||||
|
||||
|
||||
# ============================================================
|
||||
# VDirs - 常用目录表(自动构建 / --path 强制覆盖)
|
||||
# VDirs - common directory table (auto-built / forced override via --path)
|
||||
#
|
||||
# 从原 App/main.vp 的 [0/3] 段平移;本轮新增"覆盖表"语义(需求 2):
|
||||
# * 未给 --path:完全保持原自动建表(静态两条 + 各盘 Program Files + 桌面);
|
||||
# * 给过 --path:不做任何自动构建,表 = 命令行给出的路径序列(保序、去重)。
|
||||
# 其它模块(VPe/VScan)只经 DirCount()/DirAt(i) 读取本表,不交叉写全局。
|
||||
# Without --path: keep the auto table (two static entries + per-drive Program Files + desktop).
|
||||
# With --path: no auto-building; table = the command-line paths (ordered, de-duplicated).
|
||||
# Other modules (VPe/VScan) only read via 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 self-declared (no repo declaration; x64 calling convention):
|
||||
# kernel32!GetLogicalDrives / GetDriveTypeA / GetEnvironmentVariableA / SetErrorMode;
|
||||
# shell32!SHGetKnownFolderPath; ole32!CoTaskMemFree.
|
||||
# Declared per Win32 API docs (winbase.h / shlobj_core.h / combaseapi.h).
|
||||
# ============================================================
|
||||
|
||||
# Win32: typedef LONG HRESULT(有符号 32 位,负值才是失败)
|
||||
# Win32: typedef LONG HRESULT (signed 32-bit; negative = failure)
|
||||
HRESULT: t.CTypedef = int32
|
||||
# Win32: typedef const KNOWNFOLDERID *REFKNOWNFOLDERID(指向 16 字节 GUID)
|
||||
# Win32: typedef const KNOWNFOLDERID *REFKNOWNFOLDERID (points to a 16-byte GUID)
|
||||
REFKNOWNFOLDERID: t.CTypedef = t.CPtr
|
||||
|
||||
extern def GetLogicalDrives() -> DWORD
|
||||
@@ -48,38 +34,38 @@ extern def SHGetKnownFolderPath(rfid: REFKNOWNFOLDERID, dwFlags: DWORD, hToken:
|
||||
ppszPath: WCHARPTR*) -> HRESULT
|
||||
extern def CoTaskMemFree(pv: t.CPtr)
|
||||
|
||||
define SEM_FAILCRITICALERRORS = 0x0001 # SetErrorMode 标志(errhandlingapi.h)
|
||||
define KNOWNFOLDERID_SIZE = 16 # KNOWNFOLDERID / GUID 字节数
|
||||
# winnt.h: FILE_ATTRIBUTE_REPARSE_POINT 在 VScan 使用
|
||||
define SEM_FAILCRITICALERRORS = 0x0001 # SetErrorMode flag (errhandlingapi.h)
|
||||
define KNOWNFOLDERID_SIZE = 16 # KNOWNFOLDERID / GUID byte size
|
||||
# winnt.h: FILE_ATTRIBUTE_REPARSE_POINT is used by VScan
|
||||
|
||||
# GetDriveTypeA 返回值(winbase.h)
|
||||
define DRIVE_NO_ROOT_DIR = 1 # 跳过:无根路径
|
||||
define DRIVE_REMOVABLE = 2 # 探测:可移动介质(U 盘/读卡器)
|
||||
define DRIVE_FIXED = 3 # 探测:定长磁盘
|
||||
define DRIVE_REMOTE = 4 # 探测:网络盘
|
||||
define DRIVE_CDROM = 5 # 跳过:光驱
|
||||
# 决策:只跳过 NO_ROOT_DIR(1) 与 CDROM(5),其余(含 REMOVABLE)继续探测 Program Files;
|
||||
# 空介质/卡读卡器触发的弹窗与阻塞由 AddFixedDriveProgramFiles 里的 SetErrorMode 抑制。
|
||||
# GetDriveTypeA return values (winbase.h)
|
||||
define DRIVE_NO_ROOT_DIR = 1 # skip: no root path
|
||||
define DRIVE_REMOVABLE = 2 # probe: removable media (USB/card reader)
|
||||
define DRIVE_FIXED = 3 # probe: fixed disk
|
||||
define DRIVE_REMOTE = 4 # probe: network drive
|
||||
define DRIVE_CDROM = 5 # skip: CD-ROM
|
||||
# Only NO_ROOT_DIR(1) and CDROM(5) are skipped; all others (incl. REMOVABLE) probe Program Files.
|
||||
# SetErrorMode in AddFixedDriveProgramFiles suppresses popups/blocking from empty media.
|
||||
|
||||
define COMMON_DIR_CAPACITY = 128 # 表容量(内联数组元素个数)
|
||||
define DRIVE_ROOT_CAP = 8 # "X:\" + NUL 缓冲
|
||||
define COMMON_DIR_CAPACITY = 128 # table capacity (inline array element count)
|
||||
define DRIVE_ROOT_CAP = 8 # "X:\" + NUL buffer
|
||||
define DESKTOP_SUFFIX_CAP = 9 # r"\Desktop"(8) + NUL(1)
|
||||
|
||||
# 由 App/main.vp 注入
|
||||
# Injected by App/main.vp
|
||||
_mbuddy: memhub.MemBuddy*
|
||||
|
||||
# 显式长度内联数组 + 计数:避免 t.CArray[str] 单参数形态在 TPC/TransPyV 两链
|
||||
# 语义不一致(TPC=自动长度内联数组,TransPyV=指针)
|
||||
# Explicit-length inline array + count: avoids t.CArray[str] single-arg form differing
|
||||
# between TPC (auto-length inline array) and TransPyV (pointer).
|
||||
Common_Directory: t.CArray[str, COMMON_DIR_CAPACITY]
|
||||
Common_Dir_Count: int = 0 # 已用槽位数
|
||||
Common_Dir_Count: int = 0 # used slot count
|
||||
|
||||
# --path 覆盖项(保序;解析期只收集,构建期才做存在性检查并告警)
|
||||
# --path overrides (ordered; collected at parse time, existence checked and warned at build)
|
||||
Override_Dir: t.CArray[str, COMMON_DIR_CAPACITY]
|
||||
Override_Mode: int = 0 # 1 = 已出现 --path(强制替换)
|
||||
Override_Mode: int = 0 # 1 = --path seen (force replace)
|
||||
Override_Count: int = 0
|
||||
|
||||
|
||||
# 表读取接口(其它模块只经此读取,不直接触碰 Common_Directory)
|
||||
# Table read API (other modules read only through this, never touch Common_Directory)
|
||||
def DirCount() -> int:
|
||||
return Common_Dir_Count
|
||||
|
||||
@@ -92,7 +78,7 @@ def IsOverride() -> int:
|
||||
return Override_Mode
|
||||
|
||||
|
||||
# --path 首次出现时调用(幂等:仅第一次清空覆盖项);之后不再自动建表
|
||||
# Called on first --path (idempotent: clears overrides once); no auto-build afterwards
|
||||
def BeginOverride():
|
||||
global Override_Mode, Override_Count
|
||||
if Override_Mode == 0:
|
||||
@@ -100,7 +86,7 @@ def BeginOverride():
|
||||
Override_Count = 0
|
||||
|
||||
|
||||
# 收集一条 --path 覆盖项(保序;存在性检查延后到 BuildTable,以便告警落在 [0/3] 段内)
|
||||
# Collect one --path override (ordered; existence check deferred to BuildTable)
|
||||
def AddOverridePath(Path: str) -> int:
|
||||
global Override_Count
|
||||
if Path is None:
|
||||
@@ -113,8 +99,8 @@ def AddOverridePath(Path: str) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 无条件入表:仅做 strcmp 去重 + 容量上界,不做存在性检查。返回 1=已加入,0=跳过
|
||||
# 入参 Path 的生存期须覆盖整个进程(静态字面量或 MemBuddy 池内分配)。
|
||||
# Unconditional insert: strcmp de-dup + capacity bound only, no existence check. 1=added, 0=skipped.
|
||||
# Path must live for the whole process (static literal or MemBuddy pool allocation).
|
||||
def AddCommonDirForce(Path: str) -> int:
|
||||
global Common_Dir_Count
|
||||
if Path is None:
|
||||
@@ -130,8 +116,8 @@ def AddCommonDirForce(Path: str) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 加入常用目录表:存在性检查 + 去重 + 容量上界(扫描/推导出的路径走这条)。
|
||||
# 返回 1=已加入,0=跳过(不存在 / 重复 / 表满)
|
||||
# Add to the common dir table: existence check + de-dup + capacity bound (scanned/derived paths).
|
||||
# Returns 1=added, 0=skipped (missing / duplicate / table full)
|
||||
def AddCommonDir(Path: str) -> int:
|
||||
if Path is None:
|
||||
return 0
|
||||
@@ -140,11 +126,10 @@ def AddCommonDir(Path: str) -> int:
|
||||
return AddCommonDirForce(Path)
|
||||
|
||||
|
||||
# 枚举存储盘的 X:\Program Files 与 X:\Program Files (x86)。返回本次新增条目数
|
||||
# Enumerate X:\Program Files and X:\Program Files (x86) on each drive; returns added count
|
||||
def AddFixedDriveProgramFiles() -> int:
|
||||
# 进程级设置:抑制"请插入磁盘"等临界错误对话框,并让无介质驱动器立即返回错误,
|
||||
# 避免探测空读卡器/光驱时长时间阻塞。副作用:影响本进程后续所有文件操作,
|
||||
# 且本进程生命周期内持续有效(进程级、非线程级、不自动恢复)。
|
||||
# Process-level: suppress critical-error dialogs and make empty drives fail immediately,
|
||||
# avoiding long blocks. Affects all later file ops for the process lifetime (not auto-reset).
|
||||
SetErrorMode(SEM_FAILCRITICALERRORS)
|
||||
Mask: DWORD = GetLogicalDrives()
|
||||
if Mask == 0:
|
||||
@@ -160,12 +145,12 @@ def AddFixedDriveProgramFiles() -> int:
|
||||
Root[2] = '\\'
|
||||
Root[3] = 0
|
||||
Dt: UINT = GetDriveTypeA(Root)
|
||||
# 决策:只跳过 NO_ROOT_DIR(1) 与 CDROM(5);REMOVABLE(2)/FIXED(3)/
|
||||
# REMOTE(4) 都继续探测 Program Files(空介质由上方 SetErrorMode 抑制弹窗)。
|
||||
# Only NO_ROOT_DIR(1) and CDROM(5) are skipped; REMOVABLE(2)/FIXED(3)/
|
||||
# REMOTE(4) still probe Program Files (empty media handled by SetErrorMode above).
|
||||
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' 占位,下句改写为首字母
|
||||
string.strcpy(Pf, r"Q:\Program Files") # 'Q' placeholder, rewritten to the drive letter next line
|
||||
Pf[0] = char(65 + Letter)
|
||||
Added += AddCommonDir(str(Pf))
|
||||
Pfx: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
||||
@@ -176,11 +161,11 @@ def AddFixedDriveProgramFiles() -> int:
|
||||
return Added
|
||||
|
||||
|
||||
# 桌面兜底:%USERPROFILE%\Desktop(仅当 SHGetKnownFolderPath 不可用/失败时走)
|
||||
# Buf 由调用方提供(>= PATH_BUF_CAP)。返回 1=已加入,0=未加入
|
||||
# Desktop fallback: %USERPROFILE%\Desktop (only when SHGetKnownFolderPath is unavailable/fails).
|
||||
# Buf is caller-provided (>= PATH_BUF_CAP). Returns 1=added, 0=not added
|
||||
def AddDesktopFallback(Buf: bytes) -> int:
|
||||
n: DWORD = GetEnvironmentVariableA("USERPROFILE", Buf, VFs.PATH_BUF_CAP)
|
||||
# 还需追加 "\Desktop"(8 字节)+ NUL,n + 9 必须不超过缓冲容量
|
||||
# Also append "\Desktop" (8 bytes) + NUL, so n + 9 must not exceed the buffer capacity
|
||||
if n == 0 or n + uint32(DESKTOP_SUFFIX_CAP) > uint32(VFs.PATH_BUF_CAP):
|
||||
VLog.LogFmt1(" 桌面兜底: 获取 USERPROFILE 失败或过长 (win32 err=%u)\n", wb.GetLastError())
|
||||
return 0
|
||||
@@ -193,23 +178,16 @@ def AddDesktopFallback(Buf: bytes) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# 当前用户桌面:优先 SHGetKnownFolderPath(FOLDERID_Desktop)(能正确处理
|
||||
# OneDrive 等重定向场景),失败则显式退回 %USERPROFILE%\Desktop(不静默)。
|
||||
# 返回 1=已加入,0=未加入(含"不存在"如实报告)
|
||||
# Current user desktop: prefer SHGetKnownFolderPath(FOLDERID_Desktop) (handles OneDrive
|
||||
# redirection), else fall back explicitly to %USERPROFILE%\Desktop. Returns 1=added, 0=not added
|
||||
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 字节缓冲。
|
||||
# FOLDERID_Desktop {B4BFCC3A-DB2C-424C-B029-7FE99A87C641} as 16 raw LE bytes:
|
||||
# 3A CC BF B4 2C DB 4C 42 B0 29 7F E9 9A 87 C6 41 (wb.GUID mis-models Data4 as a pointer).
|
||||
GuidBuf: BYTEPTR = stdlib.malloc(KNOWNFOLDERID_SIZE)
|
||||
if GuidBuf is None:
|
||||
VLog.LogStr(" 警告: KNOWNFOLDERID 缓冲分配失败,改用兜底路径\n")
|
||||
@@ -235,7 +213,7 @@ def AddCurrentUserDesktop() -> int:
|
||||
Hr: HRESULT = SHGetKnownFolderPath(t.CPtr(GuidBuf), 0, None, &Pwsz)
|
||||
stdlib.free(t.CPtr(GuidBuf))
|
||||
if Hr < 0:
|
||||
# 规范上失败时 *ppszPath 会被置 NULL;仍防御性释放非空返回值,杜绝泄漏
|
||||
# On failure *ppszPath is NULL by spec; still free a non-NULL return defensively to avoid leaks
|
||||
if Pwsz is not None:
|
||||
CoTaskMemFree(t.CPtr(Pwsz))
|
||||
VLog.LogFmt1(" 桌面: SHGetKnownFolderPath 失败 (hr=0x%X),改用兜底路径\n", sizet(uint32(Hr)))
|
||||
@@ -244,7 +222,7 @@ def AddCurrentUserDesktop() -> int:
|
||||
VLog.LogStr(" 桌面: SHGetKnownFolderPath 返回空指针,改用兜底路径\n")
|
||||
return AddDesktopFallback(Buf)
|
||||
|
||||
# UTF-16 → UTF-8:先问所需字节数(含结尾 NUL;Needed<=0 表示失败)
|
||||
# UTF-16 -> UTF-8: query needed bytes first (incl. NUL; Needed<=0 means failure)
|
||||
Needed: int = VFs.WideToUtf8Needed(Pwsz)
|
||||
if Needed <= 0 or Needed > VFs.PATH_BUF_CAP:
|
||||
CoTaskMemFree(t.CPtr(Pwsz))
|
||||
@@ -252,7 +230,7 @@ def AddCurrentUserDesktop() -> int:
|
||||
sizet(uint32(Needed)), sizet(VFs.PATH_BUF_CAP))
|
||||
return AddDesktopFallback(Buf)
|
||||
Got: int = VFs.WideToUtf8(Pwsz, Buf, VFs.PATH_BUF_CAP)
|
||||
CoTaskMemFree(t.CPtr(Pwsz)) # 用后立即释放:成功/失败分支的同一释放点
|
||||
CoTaskMemFree(t.CPtr(Pwsz)) # Free immediately after use: single free point for both branches
|
||||
if Got == 0:
|
||||
VLog.LogFmt1(" 桌面: UTF-16→UTF-8 转换失败 (win32 err=%u),改用兜底路径\n", wb.GetLastError())
|
||||
return AddDesktopFallback(Buf)
|
||||
@@ -263,7 +241,7 @@ def AddCurrentUserDesktop() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# 打印常用目录表(逐条列举与合计属过程细节,仅 --verbose 时输出)。返回 0=表非空,1=表为空
|
||||
# Print the common dir table (verbose only). Returns 0=non-empty, 1=empty
|
||||
def PrintCommonDirs() -> int:
|
||||
if Common_Dir_Count <= 0:
|
||||
VLog.VerboseStr(" (表为空)\n")
|
||||
@@ -274,17 +252,17 @@ def PrintCommonDirs() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# 构建并打印目录表(须在 _mbuddy 注入后调用)。返回 0=表非空,1=表为空。
|
||||
# 需求 2:日志显式区分"自动表" / "覆盖表(N 条)";覆盖模式下不存在的 --path 告警并跳过。
|
||||
# Build and print the dir table (call after _mbuddy injection). Returns 0=non-empty, 1=empty.
|
||||
# Logs distinguish "auto table" / "override table (N)"; missing --paths are warned and skipped.
|
||||
def BuildTable() -> int:
|
||||
if Override_Mode == 0:
|
||||
AddCommonDirForce(r"C:\Viper") # 静态两条:无条件入表(不做存在性检查)
|
||||
AddCommonDirForce(r"C:\Viper") # Two static entries: unconditional insert (no existence check)
|
||||
AddCommonDirForce(r"D:\Viper")
|
||||
AddFixedDriveProgramFiles() # 各盘 Program Files / Program Files (x86)
|
||||
AddCurrentUserDesktop() # 当前用户桌面
|
||||
AddFixedDriveProgramFiles() # Per-drive Program Files / Program Files (x86)
|
||||
AddCurrentUserDesktop() # Current user desktop
|
||||
VLog.VerboseFmt1(" 目录表来源: 自动表 (%u 条)\n", sizet(Common_Dir_Count))
|
||||
else:
|
||||
# 强制替换:完全不做自动构建,表 = --path 序列(保序;去重由 AddCommonDirForce)
|
||||
# Force replace: no auto-build; table = --path order (de-dup handled by AddCommonDirForce)
|
||||
for i in range(Override_Count):
|
||||
if VFs.PathIsDir(Override_Dir[i]) == 0:
|
||||
VLog.LogFmtS1(" 警告: --path 不存在或非目录,跳过: %s\n", Override_Dir[i])
|
||||
|
||||
+39
-57
@@ -8,59 +8,43 @@ import w32.win32base as wb
|
||||
|
||||
|
||||
# ============================================================
|
||||
# VFs - 路径 / 文件 / 通用字节串与编码工具
|
||||
# VFs - path / file / generic byte-string and encoding helpers
|
||||
#
|
||||
# 从原 App/main.vp 平移:
|
||||
# * 通用字节串:CopyRaw / DupStr / StrEqNoCase
|
||||
# * 路径:JoinPath / PathIsDir / PathIsFile
|
||||
# * 编码:Utf8ToAnsi / AnsiToUtf8(+ 本轮新增 WideToUtf8Needed/WideToUtf8)
|
||||
# * 文件:ReadFull(+ 本轮抽出的 ReadWholeFile / WriteFileAll)
|
||||
# Generic bytes: CopyRaw / DupStr / StrEqNoCase.
|
||||
# Path: JoinPath / PathIsDir / PathIsFile.
|
||||
# Encoding: Utf8ToAnsi / AnsiToUtf8 / WideToUtf8Needed / WideToUtf8.
|
||||
# Files: 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 统一调用约定,无需修饰):
|
||||
# Win32 self-declared (no repo declaration; x64 calling convention):
|
||||
# kernel32!WideCharToMultiByte: int WideCharToMultiByte(UINT,DWORD,LPCWCH,int,LPSTR,int,LPCCH,LPBOOL);
|
||||
# 原型出处:Win32 API 文档 / winnls.h。
|
||||
# Declared per Win32 API docs / winnls.h.
|
||||
# ============================================================
|
||||
|
||||
define PATH_BUF_CAP = 512 # 单条目录/文件路径缓冲
|
||||
define CONV_CAP = 2048 # 编码转换临时缓冲
|
||||
define INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF # GetFileAttributesA 失败返回值
|
||||
define CP_UTF8 = 65001 # Win32 代码页 UTF-8
|
||||
define CP_ACP = 0 # Win32 代码页 ANSI(A 系列 API 的当前页)
|
||||
define PATH_BUF_CAP = 512 # single dir/file path buffer
|
||||
define CONV_CAP = 2048 # encoding conversion temp buffer
|
||||
define INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF # GetFileAttributesA failure return
|
||||
define CP_UTF8 = 65001 # Win32 code page UTF-8
|
||||
define CP_ACP = 0 # Win32 code page ANSI (current page for A-series APIs)
|
||||
|
||||
extern def WideCharToMultiByte(CodePage: UINT, dwFlags: DWORD, lpWideCharStr: WCHARPTR,
|
||||
cchWideChar: int, lpMultiByteStr: CHARPTR, cbMultiByte: int,
|
||||
lpDefaultChar: wb.LPCSTR, lpUsedDefaultChar: BOOL*) -> int
|
||||
|
||||
# 由 App/main.vp 注入(与其它模块同一 MemBuddy 实例)
|
||||
# Injected by App/main.vp (same MemBuddy instance as other modules)
|
||||
_mbuddy: memhub.MemBuddy*
|
||||
|
||||
# ReadWholeFile / WriteFileAll 的结果输出(模块全局)
|
||||
LastReadSize: sizet = 0 # 成功时的实际读取字节数
|
||||
LastReadErr: int = 0 # 1=打开失败 2=取大小失败 3=空文件 4=超过上限 5=分配失败
|
||||
LastReadWinErr: ULONG = 0 # 打开失败时的 GetLastError()
|
||||
LastWriteDone: sizet = 0 # WriteFileAll 实际写入字节数
|
||||
# Result outputs for ReadWholeFile / WriteFileAll (module globals)
|
||||
LastReadSize: sizet = 0 # actual bytes read on success
|
||||
LastReadErr: int = 0 # 1=open 2=size 3=empty 4=over limit 5=alloc failure
|
||||
LastReadWinErr: ULONG = 0 # GetLastError() when open fails
|
||||
LastWriteDone: sizet = 0 # bytes actually written by WriteFileAll
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 通用字节串
|
||||
# Generic byte strings
|
||||
# ============================================================
|
||||
|
||||
# 复制 Src 到 Dst(含结尾 NUL),容量不足返回 0(不写穿)
|
||||
# Copy Src to Dst incl. trailing NUL; returns 0 (no write-through) if capacity is insufficient
|
||||
def CopyRaw(Dst: bytes, Cap: int, Src: str) -> int:
|
||||
if Dst is None:
|
||||
return 0
|
||||
@@ -79,8 +63,8 @@ def CopyRaw(Dst: bytes, Cap: int, Src: str) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 用主池复制出 Src 的独立副本(返回 None 表示失败)。
|
||||
# 释放用 VFs._mbuddy.free(模块间共享同一池实例,见 main.vp 注入)。
|
||||
# Duplicate Src into the main pool (None on failure). Free via VFs._mbuddy.free
|
||||
# (the same pool instance is shared across modules).
|
||||
def DupStr(S: str) -> str:
|
||||
if S is None:
|
||||
return None
|
||||
@@ -94,7 +78,7 @@ def DupStr(S: str) -> str:
|
||||
return str(b)
|
||||
|
||||
|
||||
# ASCII 大小写无关比较(Windows 文件名不区分大小写)
|
||||
# ASCII case-insensitive compare (Windows filenames are case-insensitive)
|
||||
def StrEqNoCase(A: str, B: str) -> int:
|
||||
i: int = 0
|
||||
while True:
|
||||
@@ -112,12 +96,11 @@ def StrEqNoCase(A: str, B: str) -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 路径
|
||||
# Paths
|
||||
# ============================================================
|
||||
|
||||
# 拼接 Parent + "\" + Child 到 Out(容量 Cap),返回 1=成功,0=溢出
|
||||
# 说明:includes/os/path.vp:88 的 join 用模块全局 path._mbuddy 分配(本进程未注入 ⇒ 空指针),
|
||||
# 且无显式容量上界。故此处自拼并做上界判断。
|
||||
# Join Parent + "\" + Child into Out (capacity Cap); returns 1=ok, 0=overflow.
|
||||
# Self-contained (no includes join) because here the capacity bound must be explicit.
|
||||
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
|
||||
@@ -145,7 +128,7 @@ def JoinPath(Out: bytes, Cap: int, Parent: str, Child: str) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 路径是否为"已存在的目录":GetFileAttributesA 返回 != INVALID 且含 DIRECTORY 位
|
||||
# Whether Path is an existing directory (attrs != INVALID and DIRECTORY bit set)
|
||||
def PathIsDir(Path: str) -> int:
|
||||
Attrs: ULONG = wf.GetFileAttributesA(Path)
|
||||
if Attrs == INVALID_FILE_ATTRIBUTES:
|
||||
@@ -155,7 +138,7 @@ def PathIsDir(Path: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# 路径是否为"已存在的普通文件"(存在且非目录)
|
||||
# Whether Path is an existing regular file (exists and not a directory)
|
||||
def PathIsFile(Path: str) -> int:
|
||||
Attrs: ULONG = wf.GetFileAttributesA(Path)
|
||||
if Attrs == INVALID_FILE_ATTRIBUTES:
|
||||
@@ -166,10 +149,10 @@ def PathIsFile(Path: str) -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 编码转换(A 系列 API 走系统 ANSI;Viper 字符串约定 UTF-8)
|
||||
# Encoding conversion (A-series APIs use system ANSI; Viper strings are UTF-8)
|
||||
# ============================================================
|
||||
|
||||
# UTF-8 → ANSI(失败/非法 UTF-8 时原样拷贝,视作已是 ANSI)
|
||||
# UTF-8 -> ANSI (on failure/illegal UTF-8, copy as-is assuming already ANSI)
|
||||
def Utf8ToAnsi(Src: str, Dst: bytes, Cap: int) -> int:
|
||||
if Src is None:
|
||||
Dst[0] = 0
|
||||
@@ -191,7 +174,7 @@ def Utf8ToAnsi(Src: str, Dst: bytes, Cap: int) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# ANSI → UTF-8(失败时原样拷贝)
|
||||
# ANSI -> UTF-8 (copy as-is on failure)
|
||||
def AnsiToUtf8(Src: str, Dst: bytes, Cap: int) -> int:
|
||||
if Src is None:
|
||||
Dst[0] = 0
|
||||
@@ -213,14 +196,14 @@ def AnsiToUtf8(Src: str, Dst: bytes, Cap: int) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# UTF-16 → UTF-8 所需字节数(含结尾 NUL);<=0 表示失败
|
||||
# Bytes needed for UTF-16 -> UTF-8 (incl. trailing NUL); <=0 means failure
|
||||
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=成功
|
||||
# UTF-16 -> UTF-8 into Dst (capacity Cap); returns 1=ok
|
||||
def WideToUtf8(Src: WCHARPTR, Dst: bytes, Cap: int) -> int:
|
||||
if Src is None or Dst is None:
|
||||
return 0
|
||||
@@ -230,10 +213,10 @@ def WideToUtf8(Src: WCHARPTR, Dst: bytes, Cap: int) -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 文件读写
|
||||
# File IO
|
||||
# ============================================================
|
||||
|
||||
# 从当前文件指针读满 Want 字节(循环),返回实际读取字节数
|
||||
# Read until Want bytes from the current file pointer (looping); returns bytes read
|
||||
def ReadFull(h: wb.HANDLE, Buf: bytes, Want: sizet) -> sizet:
|
||||
total: sizet = 0
|
||||
while total < Want:
|
||||
@@ -249,9 +232,8 @@ def ReadFull(h: wb.HANDLE, Buf: bytes, Want: sizet) -> sizet:
|
||||
return total
|
||||
|
||||
|
||||
# 读取整个文件到 stdlib 分配的缓冲(含 NUL 结尾)。成功返回 bytes,调用方用 stdlib.free 释放;
|
||||
# 失败返回 None,原因见 LastReadErr(4 仅在 MaxBytes>0 时产生)。
|
||||
# Share 由调用方给出(各调用点原有的 dwShareMode 语义保持一致)。
|
||||
# Read a whole file into a stdlib buffer (NUL-terminated). Returns bytes (caller frees with
|
||||
# stdlib.free) or None; see LastReadErr. Share is caller-provided (dwShareMode).
|
||||
def ReadWholeFile(Path: str, MaxBytes: sizet, Share: ULONG) -> bytes:
|
||||
global LastReadSize, LastReadErr, LastReadWinErr
|
||||
LastReadSize = 0
|
||||
@@ -289,8 +271,8 @@ def ReadWholeFile(Path: str, MaxBytes: sizet, Share: ULONG) -> bytes:
|
||||
return Buf
|
||||
|
||||
|
||||
# 把 Buf[0..Len) 全量写入 Path(CREATE_ALWAYS)。
|
||||
# 返回 1=完整写入,0=打开/写入失败,-1=写入不完整(实际字节见 LastWriteDone)
|
||||
# Write Buf[0..Len) entirely to Path (CREATE_ALWAYS).
|
||||
# Returns 1=complete, 0=open/write failure, -1=partial (see LastWriteDone)
|
||||
def WriteFileAll(Path: str, Buf: bytes, Len: sizet) -> int:
|
||||
global LastWriteDone
|
||||
LastWriteDone = 0
|
||||
|
||||
+379
-578
File diff suppressed because it is too large.
Load diff
+11
-13
@@ -6,31 +6,29 @@ import lib.VPe as VPe
|
||||
|
||||
|
||||
# ============================================================
|
||||
# VGuid - UUID(ole32!CoCreateGuid → 8-4-4-4-12)
|
||||
# VGuid - UUID (ole32!CoCreateGuid -> 8-4-4-4-12)
|
||||
#
|
||||
# 从原 App/main.vp 平移 NewGuidStr 及 CoCreateGuid 自声明。
|
||||
# 依赖:VPe.ReadU16/ReadU32(PE 小端读取,includes 无对应辅助)、VFs 无直接依赖。
|
||||
# Depends on VPe.ReadU16/ReadU32 for little-endian reads.
|
||||
#
|
||||
# Win32 自声明(本仓无声明):
|
||||
# ole32!CoCreateGuid: HRESULT CoCreateGuid(GUID *pguid); (objbase.h;不依赖 CoInitialize)
|
||||
# HRESULT 为有符号 32 位(失败为负),与 includes/w32/win32base.vp:5 的 HANDLE 无关。
|
||||
# DLL 归属:ole32(project.vpj 已含 -lole32)。
|
||||
# Win32 self-declared (no repo declaration):
|
||||
# ole32!CoCreateGuid: HRESULT CoCreateGuid(GUID *pguid); (objbase.h; no CoInitialize needed)
|
||||
# HRESULT is signed 32-bit (negative = failure), unrelated to includes HANDLE.
|
||||
# DLL: ole32 (project.vpj already links -lole32).
|
||||
# ============================================================
|
||||
|
||||
define GUID_SIZE = 16 # GUID:Data1(4)+Data2(2)+Data3(2)+Data4(8)
|
||||
define GUID_SIZE = 16 # GUID: Data1(4)+Data2(2)+Data3(2)+Data4(8)
|
||||
define GUID_STR_CAP = 40 # "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + NUL
|
||||
|
||||
HRESULT: t.CTypedef = int32 # Win32: typedef LONG HRESULT(负值才是失败)
|
||||
HRESULT: t.CTypedef = int32 # Win32: typedef LONG HRESULT (negative = failure)
|
||||
|
||||
extern def CoCreateGuid(pguid: t.CPtr) -> HRESULT
|
||||
|
||||
# 由 App/main.vp 注入
|
||||
# Injected by App/main.vp
|
||||
_mbuddy: memhub.MemBuddy*
|
||||
|
||||
|
||||
# 生成 UUID 字符串(8-4-4-4-12,小写十六进制)写入 Out;返回 1=成功。
|
||||
# Data1/Data2/Data3 在内存中小端存放:按字段读回数值后再按 %08x/%04x 文本化,
|
||||
# Data4[8] 原样按字节文本化。
|
||||
# Write a lowercase-hex UUID (8-4-4-4-12) into Out; returns 1=ok.
|
||||
# Data1/2/3 are little-endian in memory: read as fields then format %08x/%04x; Data4[8] bytewise.
|
||||
def NewGuidStr(Out: bytes, Cap: int) -> int:
|
||||
GB: bytes = _mbuddy.alloc(GUID_SIZE)
|
||||
if GB is None:
|
||||
|
||||
+39
-102
@@ -9,83 +9,33 @@ 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) -> JsonValue*
|
||||
# json.string_val(pool, s: str) -> 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) -> JsonValue*
|
||||
# pool 形参类型 memhub.MemManager*(vtable 基类)⇒ MemBuddy / MemPool 都可传
|
||||
# (先例:includes/requests/https.vp:823,883 传 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)。
|
||||
# ============================================================
|
||||
# VJson - build the round JSON array (compact single line) with the repo json library.
|
||||
# Ownership (never freed): Json_Buf/Conv_Buf by Vet_Mb; Json_Arena by stdlib.malloc; Json_Pool on heap.
|
||||
|
||||
define JSON_BUF_CAP = 1048576 # 一轮 JSON 数组缓冲 1 MiB
|
||||
define JSON_POOL_CAP = 65536 # 逐条序列化工作池 64 KiB(每条目 reset 复用)
|
||||
define JSON_ENTRY_MAX = 4096 # 单条上限 = json 库 write 的缺省 cap(4096)
|
||||
define JSON_ENTRY_SLACK = 512 # 单条固定语法开销上界(实测 77:键/值引号 24 + 键名 40 + 冒号 6 + 逗号 5 + 大括号 2)
|
||||
define JSON_BUF_CAP = 1048576 # one round's JSON array buffer, 1 MiB
|
||||
define JSON_POOL_CAP = 65536 # per-entry serialization pool, 64 KiB (reset and reused per entry)
|
||||
define JSON_ENTRY_MAX = 4096 # max per entry = json library write default cap (4096)
|
||||
define JSON_ENTRY_SLACK = 512 # per-entry fixed syntax overhead bound (measured 77)
|
||||
|
||||
# 单条序列化在 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。
|
||||
# Upper bound of one entry's fixed pool usage (part of BuildEntry's AllocOk precheck): 7 JsonValue
|
||||
# nodes + 6 key copies + json.write output buffer (4096) + 512 slack = 5158.
|
||||
define JSON_ENTRY_POOL_FIXED = 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 只在函数体内参与算术。
|
||||
# Max length of executable / displayName values: both come from Conv_Buf, so each value is
|
||||
# <= VFs.CONV_CAP-1 chars; 2047 = 2048 - 1.
|
||||
define JSON_ENTRY_CONV_MAX = 2047
|
||||
|
||||
# 由 App/main.vp 注入
|
||||
# injected by App/main.vp
|
||||
_mbuddy: 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: memhub.MemPool* # 序列化工作池(堆上就地构造;见 JsonInit)
|
||||
Json_Buf: bytes = None # full round array text (shared by stdout and cache file)
|
||||
Json_Len: int = 0 # valid byte count of Json_Buf
|
||||
Conv_Buf: bytes = None # ANSI -> UTF-8 conversion buffer
|
||||
Json_Arena: bytes = None # serialization pool arena (stdlib.malloc, resident)
|
||||
Json_Pool: memhub.MemPool* # serialization pool (constructed in place on the heap; see JsonInit)
|
||||
|
||||
|
||||
# 分配 JSON 静态缓冲与序列化工作池(进程生命周期内复用)。返回 1=成功,0=失败
|
||||
# allocate JSON static buffers and pool (reused for process lifetime); 1 = ok, 0 = fail
|
||||
def JsonInit() -> int:
|
||||
global Json_Buf, Conv_Buf, Json_Arena, Json_Pool
|
||||
Json_Buf = _mbuddy.alloc(JSON_BUF_CAP)
|
||||
@@ -93,10 +43,8 @@ def JsonInit() -> int:
|
||||
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。
|
||||
# Construct the pool in place on the heap (memhub.NewMemPool): Json_Pool is used across frames,
|
||||
# so it must not live on this function's stack, which dies on return.
|
||||
Pool: memhub.MemPool* = memhub.NewMemPool(Json_Arena, JSON_POOL_CAP)
|
||||
if Pool is None:
|
||||
return 0
|
||||
@@ -104,7 +52,7 @@ def JsonInit() -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 供 VScan 写 stdout / VCache 写缓存文件读取结果
|
||||
# read accessor for VScan stdout / VCache cache-file writers
|
||||
def JsonBuf() -> bytes:
|
||||
return Json_Buf
|
||||
|
||||
@@ -120,7 +68,7 @@ def JReset():
|
||||
Json_Buf[0] = 0
|
||||
|
||||
|
||||
# 把字面量/已序列化文本追加到 Json_Buf(容量不足返回 0,不写穿)
|
||||
# append literal/serialized text to Json_Buf (returns 0 if it would overflow; no partial write)
|
||||
def JWrite(S: str) -> int:
|
||||
global Json_Len
|
||||
if S is None or Json_Buf is None:
|
||||
@@ -135,21 +83,16 @@ def JWrite(S: str) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 构造第 Idx 条 JSON 对象并交给库序列化为紧凑单行文本。
|
||||
# 返回该条文本(分配在 Json_Pool 上,长度 < JSON_ENTRY_MAX);None=已跳过。
|
||||
# 每条跳过的具体原因(含被处理路径)仅在 --verbose 时逐条给出;默认安静模式下由
|
||||
# EmitJson 的轮末聚合("本轮有 N 条被跳过")给出计数,既不静默异常也不逐路径刷屏。
|
||||
# Build entry Idx and serialize it to compact single-line text; returns text (in Json_Pool,
|
||||
# length < JSON_ENTRY_MAX) or None if skipped. Skip reasons are logged only with --verbose.
|
||||
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 的注释。
|
||||
# Pool capacity precheck (fail-closed): all allocations for this entry must fit, or the
|
||||
# library's pool.alloc would printf + exit(1). Runs after EmitJson's per-entry reset.
|
||||
ValNeed: sizet = sizet(JSON_ENTRY_CONV_MAX) + sizet(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")
|
||||
ValNeed = ValNeed + string.strlen(VCache.CacheSrc(Idx)) # source ("workspace"/"check")
|
||||
Need: sizet = sizet(JSON_ENTRY_POOL_FIXED) + ValNeed
|
||||
if Json_Pool.AllocOk(Need) == 0:
|
||||
VLog.VerboseFmtIS(" 警告: JSON 工作池不足(本条需 %zu 字节),已跳过: %s\n",
|
||||
@@ -159,24 +102,22 @@ def BuildEntry(Idx: int) -> str:
|
||||
if Obj == None:
|
||||
VLog.VerboseStr(" 警告: JSON 对象分配失败(工作池不足),已跳过该条目\n")
|
||||
return None
|
||||
# 键插入顺序 = 输出顺序(库按 child→next 链表输出,见 __writer.vp:92-107)。
|
||||
# executable / displayName 按既有约定转 UTF-8,且**转换后立即交给库复制**:
|
||||
# Conv_Buf 随后要复用于 displayName,晚一步用就会让 executable 串被覆盖。
|
||||
# Key insertion order = output order. Convert executable/displayName to UTF-8 and hand to the
|
||||
# library at once: Conv_Buf is reused, so a later copy would overwrite the executable string.
|
||||
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)
|
||||
# source from the cache entry (scan = "workspace"; check channel = "check")
|
||||
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)。
|
||||
# 顺带累计各值长度,供容量守卫使用。
|
||||
# Structural self-check: verify all 6 keys carry a name and a string value (a missed check
|
||||
# could emit invalid JSON); also sum value lengths for the capacity guard.
|
||||
Ok: int = 1
|
||||
N: int = 0
|
||||
Sum: sizet = 0
|
||||
@@ -192,12 +133,8 @@ def BuildEntry(Idx: int) -> str:
|
||||
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 也放得下。
|
||||
# Capacity guard: the library's write default cap=4096 returns None on overflow; guard early
|
||||
# to avoid a dropped entry. Output <= 2*Sum + SLACK(512) <= JSON_ENTRY_MAX.
|
||||
if 2 * Sum + sizet(JSON_ENTRY_SLACK) > sizet(JSON_ENTRY_MAX):
|
||||
VLog.VerboseFmtIS(" 警告: JSON 条目过长(超过 %zu 字节),已跳过: %s\n",
|
||||
sizet(JSON_ENTRY_MAX), VCache.CacheExe(Idx))
|
||||
@@ -210,11 +147,11 @@ def BuildEntry(Idx: int) -> str:
|
||||
return Txt
|
||||
|
||||
|
||||
# 把当前缓存整体构建为**单行** JSON 数组文本写入 Json_Buf(stdout 与缓存文件共用)
|
||||
# build the whole cache as a single-line JSON array into Json_Buf (shared by stdout and cache file)
|
||||
def EmitJson() -> int:
|
||||
JReset()
|
||||
if Json_Pool is None:
|
||||
# 不变式:JsonInit(VScan.ScanInit 调用)成功才会走到 EmitJson;此分支仅为不崩。
|
||||
# invariant: EmitJson is reached only after JsonInit succeeds; this branch is only to avoid a crash
|
||||
VLog.LogStr(" 警告: JSON 工作池未初始化,跳过本轮 JSON 输出\n")
|
||||
return 0
|
||||
Truncated: int = 0
|
||||
@@ -223,18 +160,18 @@ def EmitJson() -> int:
|
||||
if JWrite("[") == 0:
|
||||
return 0
|
||||
for i in range(VCache.CacheCount()):
|
||||
# 条目级回收:工作池峰值 = 单条(节点 + 字符串副本 + 库的 4096 字节输出缓冲)
|
||||
# per-entry recycle: pool peak = one entry (nodes + string copies + library 4096-byte output buffer)
|
||||
Json_Pool.reset()
|
||||
Txt: str = BuildEntry(i)
|
||||
if Txt is None:
|
||||
Skipped += 1
|
||||
else:
|
||||
n: sizet = string.strlen(Txt)
|
||||
Need: sizet = n + 3 # 自身 + 结尾 ']' + '\n' + NUL
|
||||
Need: sizet = n + 3 # self + closing ']' + '\n' + NUL
|
||||
if Count > 0:
|
||||
Need = Need + 1 # 分隔逗号
|
||||
Need = Need + 1 # separator comma
|
||||
if sizet(Json_Len) + Need > sizet(JSON_BUF_CAP):
|
||||
# 缓冲不足:跳过该条(不再像旧实现那样写出半条),末尾仍闭合 ']'
|
||||
# buffer full: skip this entry (never write a partial entry); closing ']' still emitted
|
||||
Truncated = 1
|
||||
else:
|
||||
if Count > 0:
|
||||
|
||||
+21
-36
@@ -8,35 +8,21 @@ import w32.win32base as wb
|
||||
|
||||
|
||||
# ============================================================
|
||||
# VLog - 输出原语(全部人类可读日志走 stderr;stdout 只留 JSON)
|
||||
# VLog - output primitives (all human-readable logs go to stderr; stdout keeps JSON only)
|
||||
#
|
||||
# 本模块从原 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)。
|
||||
# Verbosity: module-level Verbose_Enabled (0=quiet default, 1=--verbose). Quiet mode keeps only
|
||||
# the startup summary, heartbeat notices/failures, warnings, fatal errors and one end-of-round
|
||||
# line. VerboseStr / VerboseFmt* are siblings of Log*: they return immediately (no formatting,
|
||||
# no write) when disabled, else forward verbatim. Call sites only call Verbose*, so the switch
|
||||
# stays in this module. Set via SetVerbose before any logging.
|
||||
# ============================================================
|
||||
|
||||
define LOG_CAP = 4096 # 日志格式化缓冲
|
||||
define LOG_CAP = 4096 # Log formatting buffer
|
||||
|
||||
Log_Buffer: bytes = None # 日志格式化缓冲(stdlib.malloc,进程生命周期)
|
||||
Log_Buffer: bytes = None # Log format buffer (stdlib.malloc, process lifetime)
|
||||
|
||||
|
||||
# 字面量/已格式化字节串原样写 stderr(不自动追加换行)
|
||||
# Write literal/pre-formatted bytes to stderr verbatim (no newline appended)
|
||||
def LogStr(S: str):
|
||||
if S is None:
|
||||
return
|
||||
@@ -59,8 +45,8 @@ def LogInit() -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 固定元数的格式化转发(非 varargs 函数 ⇒ 不引入自定义可变参数风险):
|
||||
# 内部调用既有的 viperlib.snprintf,把格式化结果写 stderr。
|
||||
# Fixed-arity format forwarders (not varargs, avoiding custom variadic risk):
|
||||
# call viperlib.snprintf then write the result to stderr.
|
||||
def LogFmt1(Fmt: str, A: sizet) -> int:
|
||||
if Log_Buffer is None:
|
||||
return 0
|
||||
@@ -85,7 +71,7 @@ def LogFmt3(Fmt: str, A: sizet, B: sizet, C: sizet) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# 需求 3:扫盘进度行需要 4 个整数(r/dir/file/cand)
|
||||
# 4-integer progress line (r/dir/file/cand)
|
||||
def LogFmt4(Fmt: str, A: sizet, B: sizet, C: sizet, D: sizet) -> int:
|
||||
if Log_Buffer is None:
|
||||
return 0
|
||||
@@ -119,13 +105,13 @@ def LogFmtIS(Fmt: str, A: sizet, B: str) -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# verbose 开关与"仅 verbose 才输出"的兄弟函数
|
||||
# * 开关集中在 Verbose_Enabled 一处;调用点只写 Verbose*,不写 if。
|
||||
# * 关闭时直接返回 0(连格式化都不做),打开时逐字转发给对应 Log*。
|
||||
# * 返回码与 Log* 一致(1=已写出,0=未写出/失败)。
|
||||
# Verbose switch and "verbose-only" sibling functions
|
||||
# * switch centralized in Verbose_Enabled; call sites only call Verbose*, no if.
|
||||
# * disabled returns 0 without formatting; enabled forwards verbatim to the Log* sibling.
|
||||
# * return codes match Log* (1=written, 0=not written/failed).
|
||||
# ============================================================
|
||||
|
||||
Verbose_Enabled: int = 0 # 0=默认安静;1=--verbose
|
||||
Verbose_Enabled: int = 0 # 0=quiet default; 1=--verbose
|
||||
|
||||
|
||||
def SetVerbose(V: int):
|
||||
@@ -193,7 +179,7 @@ def VerboseFmtIS(Fmt: str, A: sizet, B: str) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# stdout 原样写(scan 模式只输出 JSON)
|
||||
# Write to stdout verbatim (scan mode outputs JSON only)
|
||||
def StdoutWrite(S: str):
|
||||
if S is None:
|
||||
return
|
||||
@@ -208,10 +194,9 @@ def StdoutWrite(S: str):
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 时间戳(需求 3:轮次开始/下轮开始)
|
||||
# 用 Win32 本地时间;AddMs 通过 FILETIME(100ns) 加法后回转 SYSTEMTIME。
|
||||
# 依据:wb.FILETIME 为 {dwLowDateTime,dwHighDateTime}(win32base.vp:36),
|
||||
# FILETIME 单位 100ns(Win32 规范),故 AddMs*10000 为增量。
|
||||
# Timestamps (round start / next round start)
|
||||
# Uses Win32 local time; AddMs adds FILETIME (100ns units) then converts back to SYSTEMTIME.
|
||||
# FILETIME is 100ns per tick, so the increment is AddMs*10000.
|
||||
# ============================================================
|
||||
|
||||
def TimeStampNow(Out: bytes, Cap: int) -> int:
|
||||
|
||||
+48
-92
@@ -13,32 +13,21 @@ 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/版本段失败原因、警告与降级说明始终输出,且不内嵌被处理路径。
|
||||
# ============================================================
|
||||
# VPe - PE/COFF parsing plus the ver subcommand flow.
|
||||
# Entry points: TryReadPeInfo/ParsePeHead/Phase1VersionSection used by VScan and ver.
|
||||
|
||||
# PE/COFF 常量
|
||||
define DOS_SIGNATURE = 0x5A4D # "MZ" 小端
|
||||
define NT_SIGNATURE = 0x00004550 # "PE\0\0" 小端
|
||||
# PE/COFF constants
|
||||
define DOS_SIGNATURE = 0x5A4D # "MZ" little-endian
|
||||
define NT_SIGNATURE = 0x00004550 # "PE\0\0" little-endian
|
||||
define OPT_MAGIC_PE32 = 0x10B
|
||||
define OPT_MAGIC_PE32P = 0x20B
|
||||
define SEC_ENTRY_SIZE = 40
|
||||
|
||||
define E_LFANEW_OFF = 0x3C # DOS 头 e_lfanew
|
||||
define COFF_MACHINE_OFF = 4 # PE签名(4)+COFF: Machine @ COFF+0
|
||||
define COFF_NUMSEC_OFF = 6 # PE签名(4)+COFF: NumberOfSections @ COFF+2
|
||||
define COFF_OPTSIZE_OFF = 20 # PE签名(4)+COFF: SizeOfOptionalHeader @ COFF+16
|
||||
define OPT_MAGIC_OFF = 24 # PE签名(4)+COFF(20) 之后为可选头 Magic
|
||||
define E_LFANEW_OFF = 0x3C # DOS header e_lfanew
|
||||
define COFF_MACHINE_OFF = 4 # PE sig(4)+COFF: Machine @ COFF+0
|
||||
define COFF_NUMSEC_OFF = 6 # PE sig(4)+COFF: NumberOfSections @ COFF+2
|
||||
define COFF_OPTSIZE_OFF = 20 # PE sig(4)+COFF: SizeOfOptionalHeader @ COFF+16
|
||||
define OPT_MAGIC_OFF = 24 # after PE sig(4)+COFF(20) comes OptionalHeader Magic
|
||||
|
||||
define SEC_NAME_OFF = 0
|
||||
define SEC_RAWSIZE_OFF = 16
|
||||
@@ -46,34 +35,29 @@ define SEC_RAWPTR_OFF = 20
|
||||
|
||||
define VERSION_SECTION = ".PRVERSN"
|
||||
|
||||
# COFF Machine 值(winnt.h)→ arch 字符串
|
||||
# COFF Machine values (winnt.h) -> arch string
|
||||
define MACHINE_X86_64 = 0x8664
|
||||
define MACHINE_X86 = 0x14C
|
||||
define MACHINE_ARM64 = 0xAA64
|
||||
define MACHINE_ARM = 0x1C0
|
||||
define MACHINE_ARMNT = 0x1C4
|
||||
|
||||
# 扫盘用 PE 读取上限
|
||||
define PE_HEAD_READ = 8192 # 单次读取的"头部窗口"(DOS+COFF+可选头+节表)
|
||||
# PE read cap for scanning
|
||||
define PE_HEAD_READ = 8192 # single-read head window (DOS+COFF+optional header+section table)
|
||||
|
||||
define VER_CAP = 128 # .PRVERSN 版本串缓冲
|
||||
define ARCH_CAP = 32 # arch 字符串缓冲("unknown(0xFFFF)" 14 字节)
|
||||
define VER_CAP = 128 # .PRVERSN version string buffer
|
||||
define ARCH_CAP = 32 # arch string buffer ("unknown(0xFFFF)" is 14 bytes)
|
||||
|
||||
# ParsePeHead 的输出(用模块全局而非数组出参)
|
||||
# ParsePeHead outputs (module globals, not array outparams)
|
||||
Pe_RawPtr: uint32 = 0
|
||||
Pe_RawSize: uint32 = 0
|
||||
|
||||
# TryReadPeInfo 打开失败(rc=1)时的 GetLastError(check 通道据此区分 NOT_FOUND / NO_PERMISSION)。
|
||||
# 生命周期:TryReadPeInfo 每次进入复位为 0;仅在 CreateFileA 返回 INVALID_HANDLE_VALUE 时写入,
|
||||
# 且**紧跟该调用读取**(其后任何 Win32 调用——包括 PeDbgPrint 里的 WriteFile——都会覆盖它)。
|
||||
# GetLastError when TryReadPeInfo open fails (rc=1); the check channel maps it to
|
||||
# NOT_FOUND/NO_PERMISSION. Reset on entry, written right after CreateFileA (any call clobbers it).
|
||||
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 每次进入复位。
|
||||
# Failure-path diagnostics for the [pe] line: Pe_DbgGot (bytes parsed), Pe_DbgSecOff (section table
|
||||
# offset), Pe_DbgLastRc (last rc). Reset on entry to TryReadPeInfo and Phase1VersionSection.
|
||||
Pe_DbgLfanew: uint32 = 0
|
||||
Pe_DbgNsec: uint32 = 0
|
||||
Pe_DbgOptSize: uint32 = 0
|
||||
@@ -82,9 +66,7 @@ Pe_DbgGot: sizet = 0
|
||||
Pe_DbgLastRc: int = 0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 小端读取(Buf 为无符号字节基址,Buf[i] 即 u8)
|
||||
# ============================================================
|
||||
# ---- Little-endian reads (Buf is an unsigned byte base; Buf[i] is u8) ----
|
||||
|
||||
def ReadU16(Buf: BYTEPTR, Off: int) -> uint32:
|
||||
Val: uint32 = uint32(Buf[Off])
|
||||
@@ -100,18 +82,16 @@ def ReadU32(Buf: BYTEPTR, Off: int) -> uint32:
|
||||
return Val
|
||||
|
||||
|
||||
# 复制 8 字节节名并补 NUL,写入 Out(调用方保证 >=9 字节)
|
||||
# copy an 8-byte section name and NUL-terminate into Out (caller guarantees >=9 bytes)
|
||||
def CopySectionName(Buf: BYTEPTR, Off: int, Out: bytes):
|
||||
for k in range(8):
|
||||
Out[k] = Buf[Off + k]
|
||||
Out[8] = 0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# PE 解析(轻量,无诊断输出;供 scan 与 ver 共用同一套常量/偏移)
|
||||
# ============================================================
|
||||
# ---- PE parsing (lightweight, no diagnostics; scan and ver share the same constants/offsets) ----
|
||||
|
||||
# COFF Machine → arch 字符串。返回 1=已写入
|
||||
# COFF Machine -> arch string; returns 1 when written
|
||||
def FillArch(OutArch: bytes, Cap: int, Machine: int) -> int:
|
||||
if Cap < 24:
|
||||
return 0
|
||||
@@ -128,10 +108,8 @@ def FillArch(OutArch: bytes, Cap: int, Machine: int) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# [诊断] 打印一行 PE 解析中间量(单行由多次固定元数转发拼出,不引入自定义 varargs)。
|
||||
# Rc:失败码;Fsz:文件大小(该点尚不可知时传 0);RawPtr/RawSize:.PRVERSN 段位置(尚不可知时传 0)。
|
||||
# sectab_end = sec_off + nsec * 40,由打印时当场推算(尚未解析出的量保持 0)。
|
||||
# 属"内部路径细节"(逐次解析中间量),默认安静,仅 --verbose 时输出。
|
||||
# [diag] print one PE parse intermediate line (assembled from fixed-arity calls). Rc = failure
|
||||
# code, Fsz = file size, RawPtr/RawSize = .PRVERSN position; process detail, --verbose only.
|
||||
def PeDbgPrint(Rc: int, Fsz: sizet, RawPtr: uint32, RawSize: uint32):
|
||||
global Pe_DbgLastRc
|
||||
Pe_DbgLastRc = Rc
|
||||
@@ -143,20 +121,14 @@ def PeDbgPrint(Rc: int, Fsz: sizet, RawPtr: uint32, RawSize: uint32):
|
||||
VLog.VerboseFmt2(" rawptr=%u rawsize=%u\n", sizet(RawPtr), sizet(RawSize))
|
||||
|
||||
|
||||
# 取"最近一次 TryReadPeInfo 的 CreateFileA 失败错误码"(rc=1 时才有意义;其余情况为 0)。
|
||||
# check 通道按此映射:2/3(NOT_FOUND) → NOT_FOUND;5/32 → NO_PERMISSION;其它 → NO_PERMISSION。
|
||||
# Get the last CreateFileA failure code (meaningful only when rc=1, else 0). The check channel
|
||||
# maps it: 2/3 (NOT_FOUND) -> NOT_FOUND; 5/32 and others -> 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] 行。
|
||||
# Pure parse: from the head-window Buf (Got bytes) find .PRVERSN raw_ptr/raw_size (globals
|
||||
# Pe_RawPtr/Pe_RawSize) and fill OutArch. Returns 0 = found; 1..7 = failure kinds.
|
||||
def ParsePeHead(Buf: bytes, Got: sizet, OutArch: bytes, ArchCap: int) -> int:
|
||||
global Pe_RawPtr, Pe_RawSize
|
||||
global Pe_DbgLfanew, Pe_DbgNsec, Pe_DbgOptSize, Pe_DbgSecOff, Pe_DbgGot, Pe_DbgLastRc
|
||||
@@ -197,7 +169,7 @@ def ParsePeHead(Buf: bytes, Got: sizet, OutArch: bytes, ArchCap: int) -> int:
|
||||
Pe_DbgSecOff = uint32(sec_off)
|
||||
if sizet(sec_off) + sizet(nsec) * sizet(SEC_ENTRY_SIZE) > Got:
|
||||
Pe_DbgLastRc = 6
|
||||
return 6 # 节表超出头部窗口(极端多节 PE,跳过)
|
||||
return 6 # section table beyond head window (extreme many-section PE; skip)
|
||||
for i in range(int(nsec)):
|
||||
entry: int = sec_off + i * SEC_ENTRY_SIZE
|
||||
if string.strncmp(str(Buf + entry + SEC_NAME_OFF), VERSION_SECTION, 8) == 0:
|
||||
@@ -208,7 +180,7 @@ def ParsePeHead(Buf: bytes, Got: sizet, OutArch: bytes, ArchCap: int) -> int:
|
||||
return 7
|
||||
|
||||
|
||||
# 把 Src[Off .. Off+Len) 中的版本串(到第一个 NUL 或 VerCap-1)拷入 OutVer
|
||||
# copy the version string in Src[Off..Off+Len) (to first NUL or VerCap-1) into OutVer
|
||||
def CopyVersion(Src: bytes, Off: int, Len: int, OutVer: bytes, VerCap: int) -> int:
|
||||
n: int = 0
|
||||
while n < Len and n < VerCap - 1:
|
||||
@@ -220,20 +192,13 @@ def CopyVersion(Src: bytes, Off: int, Len: int, OutVer: bytes, VerCap: int) -> i
|
||||
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)。
|
||||
# Lightweight PE probe for scanning: open once, read the head window to parse the section table,
|
||||
# fall back to a whole-file read if needed. Returns 0 with OutVer/OutArch, else non-zero skip.
|
||||
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)
|
||||
# reset diagnostics on entry so this [pe] line reflects only this parse
|
||||
Pe_DbgLfanew = 0
|
||||
Pe_DbgNsec = 0
|
||||
Pe_DbgOptSize = 0
|
||||
@@ -246,7 +211,7 @@ def TryReadPeInfo(Target: str, OutVer: bytes, VerCap: int, OutArch: bytes, ArchC
|
||||
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 的写日志会覆盖)
|
||||
Pe_OpenErr = wb.GetLastError() # read right after CreateFileA (PeDbgPrint's log would clobber it)
|
||||
PeDbgPrint(1, sizet(0), uint32(0), uint32(0))
|
||||
return 1
|
||||
li: wb.LARGE_INTEGER = wb.LARGE_INTEGER()
|
||||
@@ -291,13 +256,11 @@ def TryReadPeInfo(Target: str, OutVer: bytes, VerCap: int, OutArch: bytes, ArchC
|
||||
return 6
|
||||
|
||||
if sizet(raw_ptr) + sizet(raw_size) <= got:
|
||||
# 段内容已在头部窗口内
|
||||
# section content is within the head window
|
||||
CopyVersion(head, int(raw_ptr), int(raw_size), OutVer, VerCap)
|
||||
else:
|
||||
# 段内容不在头部窗口内:改走"整文件读"路径(与 ver [1/3] 同一套 API,已验证可用)。
|
||||
# 这里不用 wf.SetFilePointerEx 定位:其 liDistanceToMove 在真实 Win32 上按值传 8 字节,
|
||||
# 传地址时"距离"会变成该地址值,文件指针被推到 EOF 之后,后续 ReadFile 只能读回 0 字节。
|
||||
# VFs 据此实现的 SeekTo 已删除(零调用点),见 VFs.vp:19-24。
|
||||
# Section content is outside the head window: read the whole file (same API as ver [1/3]).
|
||||
# Do not seek: wf.SetFilePointerEx passes liDistanceToMove by value, so an address breaks it.
|
||||
stdlib.free(t.CPtr(head))
|
||||
wb.CloseHandle(h)
|
||||
whole: bytes = VFs.ReadWholeFile(
|
||||
@@ -315,16 +278,11 @@ def TryReadPeInfo(Target: str, OutVer: bytes, VerCap: int, OutArch: bytes, ArchC
|
||||
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
|
||||
# ============================================================
|
||||
# ver [1/3]: open the target exe, parse the section table and read .PRVERSN. Returns 0 = found
|
||||
# and passed, 1 = failure. Independent parse; [pe] rc encoded like ParsePeHead (1..7).
|
||||
def Phase1VersionSection(Target: str) -> int:
|
||||
global Pe_DbgLfanew, Pe_DbgNsec, Pe_DbgOptSize, Pe_DbgSecOff, Pe_DbgGot, Pe_DbgLastRc
|
||||
# 每次进入先复位诊断中间量:本次 [pe] 行只反映本次解析(尚未解析出的量保持 0)
|
||||
# reset diagnostics on entry so this [pe] line reflects only this parse
|
||||
Pe_DbgLfanew = 0
|
||||
Pe_DbgNsec = 0
|
||||
Pe_DbgOptSize = 0
|
||||
@@ -355,14 +313,14 @@ def Phase1VersionSection(Target: str) -> int:
|
||||
|
||||
ub: BYTEPTR = BYTEPTR(buf)
|
||||
|
||||
# DOS 头
|
||||
# DOS header
|
||||
if ReadU16(ub, 0) != DOS_SIGNATURE:
|
||||
VLog.LogStr(" 不是有效的 PE 文件(缺少 MZ 头)\n")
|
||||
PeDbgPrint(2, total, uint32(0), uint32(0))
|
||||
stdlib.free(t.CPtr(buf))
|
||||
return 1
|
||||
|
||||
# NT 头
|
||||
# NT header
|
||||
lfanew: uint32 = ReadU32(ub, E_LFANEW_OFF)
|
||||
Pe_DbgLfanew = lfanew
|
||||
if sizet(lfanew) + 24 > total:
|
||||
@@ -395,7 +353,7 @@ def Phase1VersionSection(Target: str) -> int:
|
||||
stdlib.free(t.CPtr(buf))
|
||||
return 1
|
||||
|
||||
# 查找 .PRVERSN
|
||||
# find .PRVERSN
|
||||
found: int = 0
|
||||
raw_ptr: uint32 = 0
|
||||
raw_size: uint32 = 0
|
||||
@@ -420,7 +378,7 @@ def Phase1VersionSection(Target: str) -> int:
|
||||
stdlib.free(t.CPtr(buf))
|
||||
return 1
|
||||
|
||||
# 取到第一个 NUL 为止
|
||||
# up to the first NUL
|
||||
if sizet(raw_ptr) + sizet(raw_size) > total:
|
||||
VLog.LogFmt2(" 段内容越界 (offset=%u size=%u)\n", raw_ptr, raw_size)
|
||||
PeDbgPrint(6, total, raw_ptr, raw_size)
|
||||
@@ -436,15 +394,13 @@ def Phase1VersionSection(Target: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ver 子命令:沿用原有 [0/3]→[1/3]→[2/3] 检查与返回码(日志走 stderr)
|
||||
# ============================================================
|
||||
# ver subcommand: [0/3] -> [1/3] -> [2/3] checks and exit codes (logs to 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 时输出
|
||||
# banner (no path) is kept; the target path is process detail, --verbose only
|
||||
VLog.LogFmtS1("VET - Viper Environment Tool %s\n\n", VCli.Version())
|
||||
VLog.VerboseFmtS1("目标: %s\n\n", target)
|
||||
|
||||
|
||||
+44
-75
@@ -11,61 +11,44 @@ import lib.VLog as VLog
|
||||
|
||||
|
||||
# ============================================================
|
||||
# VProc - 带超时的子进程执行(<exe> --version)
|
||||
# VProc - subprocess execution with timeout (<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 成功后立即关闭。
|
||||
# Runs CreatePipe + STARTUPINFOA + CreateProcessA + pipe read with a timeout:
|
||||
# WaitForSingleObject(hProcess, 5000); on WAIT_TIMEOUT it does TerminateProcess + wait(INFINITE)
|
||||
# and marks a timeout. Every path closes the process/thread/read-pipe handles; the write end is
|
||||
# closed by the parent right after a successful CreateProcessA.
|
||||
#
|
||||
# 结构体:直接用 w32 声明的类型化结构体(wb.SECURITY_ATTRIBUTES / wp.STARTUPINFOA /
|
||||
# wp.PROCESS_INFORMATION),字段赋值 + __sizeof__(),与 includes/subprocess.vp:78-123
|
||||
# 的已验证写法一致;不再用裸字节缓冲 + 偏移写入。
|
||||
# Uses w32 typed structs (wb.SECURITY_ATTRIBUTES / wp.STARTUPINFOA / wp.PROCESS_INFORMATION)
|
||||
# with field assignment + __sizeof__().
|
||||
#
|
||||
# 关键:`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)。
|
||||
# Note: `bytes` is a pointer type, so c.Addr(Buf) yields the address of the pointer slot
|
||||
# (char**), not the buffer; the buffer address is Buf itself.
|
||||
# ============================================================
|
||||
|
||||
define VERSION_PROBE_TIMEOUT_MS = 5000 # --version 探测超时(毫秒)
|
||||
define VERSION_PROBE_DIAG_CAP = 4096 # ver [2/3] 诊断输出缓冲
|
||||
define PROBE_CMD_EXTRA_CAP = 32 # 命令缓冲额外量(引号 + " --version" + NUL)
|
||||
define PROBE_TARGET_MAX = 4096 # 目标路径上界(超限直接判启动失败,不构造命令)
|
||||
define PROBE_LINE_CAP = 208 # 探测输出"首行"诊断缓冲(200 字符 + NUL + 对齐)
|
||||
define VERSION_PROBE_TIMEOUT_MS = 5000 # --version probe timeout (ms)
|
||||
define VERSION_PROBE_DIAG_CAP = 4096 # ver [2/3] diagnostic output buffer
|
||||
define PROBE_CMD_EXTRA_CAP = 32 # command buffer extra (quotes + " --version" + NUL)
|
||||
define PROBE_TARGET_MAX = 4096 # target path bound (over limit = launch failure, no command)
|
||||
define PROBE_LINE_CAP = 208 # first-line diagnostic buffer (200 chars + NUL + align)
|
||||
define VERSION_PREFIX = "Viper"
|
||||
|
||||
# 结果输出(模块全局);RunVersionProbe 每次进入先复位
|
||||
# Result outputs (module globals); reset on each RunVersionProbe entry
|
||||
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 的出口写入。
|
||||
# Last launch failure (rc=1): stage (0=none 1=bad args 2=cmd alloc 3=CreatePipe 4=CreateProcessA)
|
||||
# and GetLastError read immediately after the call. Reset on entry; written only at rc=1 exits.
|
||||
Probe_LaunchStage: int = 0
|
||||
Probe_LaunchErr: ULONG = 0
|
||||
|
||||
|
||||
# 前缀经函数暴露给 VScan(跨模块函数调用路径明确支持;避免跨模块字符串 t.CDefine 取值)
|
||||
# Prefix exposed to VScan via a function (cross-module; avoids cross-module string t.CDefine read)
|
||||
def VersionPrefix() -> str:
|
||||
return VERSION_PREFIX
|
||||
|
||||
|
||||
# 捕获输出前 5 字符是否为 "Viper"(口径与 ver [2/3] / scan 候选过滤逐字一致:
|
||||
# string.strncmp(Out, "Viper", 5) == 0;Out 短于 5 字符时 strncmp 在 NUL 处停下 ⇒ 不通过)。
|
||||
# Whether the captured output starts with "Viper" (strncmp(Out, "Viper", 5) == 0).
|
||||
# Output shorter than 5 chars stops at NUL and thus fails.
|
||||
def OutputHasViperPrefix(Out: str) -> int:
|
||||
if Out is None:
|
||||
return 0
|
||||
@@ -74,7 +57,7 @@ def OutputHasViperPrefix(Out: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# 取最近一次拉起失败的阶段 / 错误码(rc=1 时才有意义)
|
||||
# Last launch-failure stage / error code (meaningful only when rc=1)
|
||||
def ProbeLaunchStage() -> int:
|
||||
return Probe_LaunchStage
|
||||
|
||||
@@ -83,8 +66,8 @@ def ProbeLaunchErr() -> ULONG:
|
||||
return Probe_LaunchErr
|
||||
|
||||
|
||||
# [诊断] 取捕获输出的首行(到 NUL/CR/LF 或 Cap-1 字符为止),写入 Out(NUL 结尾)。
|
||||
# 返回写入长度。上界由 Out 中的 NUL(ReadFile 循环末尾所写)保证,不会越读。
|
||||
# [diag] Copy the first line of output (up to NUL/CR/LF or Cap-1) into Out (NUL-terminated).
|
||||
# Returns the written length; the NUL written by the read loop bounds it, so no overread.
|
||||
def CopyFirstLine(Src: bytes, Out: bytes, Cap: int) -> int:
|
||||
ub: BYTEPTR = BYTEPTR(Src)
|
||||
n: int = 0
|
||||
@@ -99,17 +82,8 @@ def CopyFirstLine(Src: bytes, Out: bytes, Cap: int) -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 执行 "<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* 给出(默认不逐路径刷屏)。
|
||||
# Run "<exe> --version" with timeout. Out/OutCap: merged stdout+stderr buffer (NUL-terminated).
|
||||
# Trace=1 (scan path) prints before/after lines; returns 0=normal, 1=launch failure, 2=timeout.
|
||||
# ============================================================
|
||||
def RunVersionProbe(Target: str, Out: bytes, OutCap: sizet, Trace: int) -> int:
|
||||
global Probe_ExitCode, Probe_TimedOut, Probe_LaunchStage, Probe_LaunchErr
|
||||
@@ -131,7 +105,7 @@ def RunVersionProbe(Target: str, Out: bytes, OutCap: sizet, Trace: int) -> int:
|
||||
if Trace != 0:
|
||||
VLog.VerboseFmtS1(" [--version] 开始: %s\n", Target)
|
||||
|
||||
# --- 命令缓冲:"<exe>" --version ---
|
||||
# --- command buffer: "<exe>" --version ---
|
||||
n: sizet = string.strlen(Target) + sizet(PROBE_CMD_EXTRA_CAP)
|
||||
CmdLine: bytes = stdlib.malloc(n)
|
||||
if CmdLine is None:
|
||||
@@ -140,29 +114,28 @@ def RunVersionProbe(Target: str, Out: bytes, OutCap: sizet, Trace: int) -> int:
|
||||
return 1
|
||||
viperlib.snprintf(CmdLine, n, "\"%s\" --version", Target)
|
||||
|
||||
# --- SECURITY_ATTRIBUTES(栈上结构体;必须在 CreatePipe 之前填好) ---
|
||||
# nLength 必须等于 sizeof(SECURITY_ATTRIBUTES)=24,否则 CreatePipe 直接失败
|
||||
# (ERROR_INVALID_PARAMETER);lpSecurityDescriptor=NULL 表示默认安全描述符。
|
||||
# --- SECURITY_ATTRIBUTES (stack struct, filled before CreatePipe) ---
|
||||
# nLength must equal sizeof(SECURITY_ATTRIBUTES)=24 or CreatePipe fails; NULL descriptor = default.
|
||||
Sa: wb.SECURITY_ATTRIBUTES = wb.SECURITY_ATTRIBUTES()
|
||||
Sa.nLength = uint32(wb.SECURITY_ATTRIBUTES.__sizeof__())
|
||||
Sa.lpSecurityDescriptor = t.CPtr(0)
|
||||
Sa.bInheritHandle = 1
|
||||
|
||||
# --- 管道(可继承;父进程随后关写端) ---
|
||||
# --- pipe (inheritable; parent closes the write end afterwards) ---
|
||||
hRead: wb.HANDLE = None
|
||||
hWrite: wb.HANDLE = None
|
||||
if wf.CreatePipe(&hRead, &hWrite,
|
||||
&Sa, 0) == 0:
|
||||
PipeErr: ULONG = wb.GetLastError() # 紧跟 CreatePipe 读取
|
||||
PipeErr: ULONG = wb.GetLastError() # read right after CreatePipe
|
||||
Probe_LaunchStage = 3
|
||||
Probe_LaunchErr = PipeErr
|
||||
stdlib.free(t.CPtr(CmdLine))
|
||||
VLog.LogStr(" 警告: --version 探测启动失败 (rc=1, CreatePipe)\n")
|
||||
return 1
|
||||
# 读端不继承给子进程(写端继承由 SECURITY_ATTRIBUTES.bInheritHandle 决定)
|
||||
# The read end is not inherited (write-end inheritance is set by bInheritHandle)
|
||||
wf.SetHandleInformation(hRead, wf.HANDLE_FLAG_INHERIT, 0)
|
||||
|
||||
# --- STARTUPINFOA:cb=sizeof, dwFlags=STARTF_USESTDHANDLES, hStdOutput=hStdError=写端 ---
|
||||
# --- STARTUPINFOA: cb=sizeof, dwFlags=STARTF_USESTDHANDLES, hStdOutput=hStdError=write end ---
|
||||
Si: wp.STARTUPINFOA = wp.STARTUPINFOA()
|
||||
Si.cb = uint32(wp.STARTUPINFOA.__sizeof__())
|
||||
Si.dwFlags = wp.STARTF_USESTDHANDLES
|
||||
@@ -171,11 +144,11 @@ def RunVersionProbe(Target: str, Out: bytes, OutCap: sizet, Trace: int) -> int:
|
||||
|
||||
Pi: wp.PROCESS_INFORMATION = wp.PROCESS_INFORMATION()
|
||||
|
||||
# --- CreateProcessA(bInheritHandles=TRUE;CREATE_NO_WINDOW:输出走管道,不开控制台窗口) ---
|
||||
# --- CreateProcessA (bInheritHandles=TRUE; CREATE_NO_WINDOW: pipe output, no console 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),
|
||||
&Si, &Pi)
|
||||
# GetLastError 紧跟调用读取:其后 stdlib.free 等都会覆盖它
|
||||
# GetLastError read immediately: later stdlib.free etc. would overwrite it
|
||||
CreateErr: ULONG = wb.GetLastError()
|
||||
stdlib.free(t.CPtr(CmdLine))
|
||||
if Ok == 0:
|
||||
@@ -187,21 +160,21 @@ def RunVersionProbe(Target: str, Out: bytes, OutCap: sizet, Trace: int) -> int:
|
||||
sizet(CreateErr))
|
||||
return 1
|
||||
|
||||
# 父进程关闭写端(子进程已持有其副本);此后管道只由子进程持有写端
|
||||
# Parent closes the write end (child holds a copy); afterwards only the child owns it
|
||||
wb.CloseHandle(hWrite)
|
||||
|
||||
# --- 超时等待:先等进程退出,再读干管道 ---
|
||||
# --- timeout wait: wait for exit, then drain the pipe ---
|
||||
TimedOut: int = 0
|
||||
KillMs: sizet = 0
|
||||
Wr: ULONG = wsync.WaitForSingleObject(Pi.hProcess, VERSION_PROBE_TIMEOUT_MS)
|
||||
if Wr != wb.WAIT_OBJECT_0:
|
||||
# WAIT_TIMEOUT(或 WAIT_FAILED 等):杀进程并回收,避免悬挂
|
||||
# WAIT_TIMEOUT (or WAIT_FAILED): kill and reap the process to avoid hanging
|
||||
TimedOut = 1
|
||||
KillMs = sizet(wb.GetTickCount64() - T0)
|
||||
wp.TerminateProcess(Pi.hProcess, 1)
|
||||
wsync.WaitForSingleObject(Pi.hProcess, wb.INFINITE)
|
||||
|
||||
# 读干管道:子进程已退出/被杀 ⇒ 写端全关,ReadFile 到 EOF 返回
|
||||
# Drain the pipe: the child has exited/been killed, so the write end is closed and ReadFile hits EOF
|
||||
total: sizet = 0
|
||||
while total + 1 < OutCap:
|
||||
br: ULONG = 0
|
||||
@@ -224,8 +197,8 @@ def RunVersionProbe(Target: str, Out: bytes, OutCap: sizet, Trace: int) -> int:
|
||||
|
||||
Elapsed: sizet = sizet(wb.GetTickCount64() - T0)
|
||||
if TimedOut != 0:
|
||||
# 超时:写明等待结果(WAIT_TIMEOUT=258 / WAIT_FAILED=4294967295 等)与等待时长。
|
||||
# 该候选路径与"超时前已捕获内容"属逐次探测细节,仅 --verbose 时给出(默认不逐路径刷屏)
|
||||
# Timeout: report the wait result (WAIT_TIMEOUT=258 / WAIT_FAILED=4294967295) and elapsed.
|
||||
# The candidate path and captured content are per-probe detail shown only with --verbose.
|
||||
VLog.LogFmt3(" 警告: --version 探测超时 (rc=2, wait=%u, 上限 %u ms),已等待 %u ms 后终止子进程\n",
|
||||
sizet(Wr), sizet(VERSION_PROBE_TIMEOUT_MS), KillMs)
|
||||
if total == 0:
|
||||
@@ -238,8 +211,7 @@ def RunVersionProbe(Target: str, Out: bytes, OutCap: sizet, Trace: int) -> int:
|
||||
VLog.VerboseFmt4(" [--version] 结束: 耗时 %u ms, 退出码 %u, 超时 %u, 输出 %u 字节\n",
|
||||
Elapsed, sizet(ExitCode), sizet(TimedOut), total)
|
||||
if TimedOut == 0:
|
||||
# 正常结束(rc=0):捕获字节数与首行属逐次探测细节,仅 --verbose 时输出
|
||||
# (ver 路径的 [2/3] 结果仍由 RunVersionPrefixCheck 照常给出)。
|
||||
# Normal exit (rc=0): captured byte count and first line are per-probe detail (verbose only).
|
||||
Line: bytes = stdlib.malloc(PROBE_LINE_CAP)
|
||||
if Line is None:
|
||||
VLog.VerboseFmt1(" [--version] 捕获 %u 字节(首行缓冲分配失败)\n", total)
|
||||
@@ -256,9 +228,8 @@ def RunVersionProbe(Target: str, Out: bytes, OutCap: sizet, Trace: int) -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ver [2/3]:执行 <exe> --version,校验捕获输出前 5 字符是否为 "Viper"
|
||||
# 返回 0=通过,1=失败(启动失败/超时/前缀不符)
|
||||
# 行为增强:改用带超时探测(原实现无超时),其余日志与判定口径不变。
|
||||
# ver [2/3]: run <exe> --version and check the first 5 output chars are "Viper".
|
||||
# Returns 0=pass, 1=fail (launch failure / timeout / prefix mismatch).
|
||||
# ============================================================
|
||||
def RunVersionPrefixCheck(Target: str) -> int:
|
||||
OutCap: sizet = sizet(VERSION_PROBE_DIAG_CAP)
|
||||
@@ -310,10 +281,8 @@ def RunVersionPrefixCheck(Target: str) -> int:
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 结构体布局(x64 / LLP64):由 w32 声明的类型化结构体 + __sizeof__() 决定,
|
||||
# 本模块不再手工推导偏移。已核验(只读,本机 Python ctypes,未编译任何东西):
|
||||
# Struct layout (x64 / LLP64), from the w32 typed structs + __sizeof__():
|
||||
# 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 的字段声明一致。
|
||||
# ============================================================
|
||||
+99
-199
@@ -18,64 +18,28 @@ 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)。
|
||||
# ============================================================
|
||||
# VScan - scan subcommand: directory tree walk, candidate filtering, progress, round driver.
|
||||
# Contract: stdout = one full JSON array per round; stderr = logs (quiet by default, --verbose details).
|
||||
|
||||
define MAX_SCAN_DEPTH = 6 # 递归深度上限
|
||||
define PACE_DIR_UNITS = 16 # 每处理 16 个目录项
|
||||
define PACE_FILE_UNITS = 64 # 每处理 64 个文件
|
||||
define PACE_SLEEP_MS = 20 # 上述节流点 Sleep(20)
|
||||
define PROG_INTERVAL_MS = 1000 # 进度行最短间隔(需求 3)
|
||||
define PROG_STAMP_CAP = 64 # 时间戳文本缓冲
|
||||
define PROG_ELAPSED_CAP = 32 # "3.2s" 文本缓冲
|
||||
define REJECT_LOG_MAX = 20 # [reject] 逐条打印上限(超出后只在轮末打一次汇总)
|
||||
# WIN32_FIND_DATAA:4+8+8+8+4+4+4+4 = 44 之后为 cFileName(见"API 证据表"推导)
|
||||
define MAX_SCAN_DEPTH = 6 # max recursion depth
|
||||
define PACE_DIR_UNITS = 16 # sleep every 16 directory entries
|
||||
define PACE_FILE_UNITS = 64 # sleep every 64 files
|
||||
define PACE_SLEEP_MS = 20 # Sleep(20) at each throttle point
|
||||
define PROG_INTERVAL_MS = 1000 # min interval between progress lines
|
||||
define PROG_STAMP_CAP = 64 # timestamp text buffer
|
||||
define PROG_ELAPSED_CAP = 32 # "3.2s" elapsed text buffer
|
||||
define REJECT_LOG_MAX = 20 # max per-line [reject] logs; rest summarized at round end
|
||||
# WIN32_FIND_DATAA: cFileName starts at offset 44
|
||||
define FD_ATTRIBUTES_OFF = 0
|
||||
define FD_FILENAME_OFF = 44
|
||||
define FIND_DATA_SIZE = 320 # sizeof(WIN32_FIND_DATAA):318 对齐到 4 = 320
|
||||
# winnt.h: FILE_ATTRIBUTE_REPARSE_POINT(符号链接/挂载点等重解析点,递归时跳过以防成环)
|
||||
define FIND_DATA_SIZE = 320 # sizeof(WIN32_FIND_DATAA): 318 aligned to 4 = 320
|
||||
# winnt.h: reparse points (symlinks/mounts); skipped in recursion to avoid cycles
|
||||
define FILE_ATTRIBUTE_REPARSE_POINT = 0x400
|
||||
|
||||
# 由 App/main.vp 注入
|
||||
# injected by App/main.vp
|
||||
_mbuddy: memhub.MemBuddy*
|
||||
|
||||
# 扫盘静态缓冲(进程生命周期内复用;单线程,无并发)
|
||||
# scan static buffers (reused for process lifetime; single-threaded)
|
||||
Scr_Ver: bytes = None
|
||||
Scr_Arch: bytes = None
|
||||
Scr_Id: bytes = None
|
||||
@@ -84,24 +48,23 @@ Probe_Buf: bytes = None
|
||||
Prog_Stamp: bytes = None
|
||||
Prog_Elapsed: bytes = None
|
||||
|
||||
# 节流开关:1=启用节流 sleep(常驻模式默认),0=禁用(--once 单轮扫描时由 RunScan 置 0)
|
||||
# throttle sleep switch: 1 = on (resident default), 0 = off (RunScan sets 0 for --once)
|
||||
Pace_Enabled: int = 1
|
||||
|
||||
# "常规轮次"应当采用的节流开关值(由 RunScan 一次性算出):Pace_Enabled 是**本轮**的实际值,
|
||||
# 每轮开头由 RunScanRound 从本值恢复,再叠加 --first-no-pace 的"第 1 轮不节流"。
|
||||
# 拆成两个变量是为了让"首轮特例"只影响第 1 轮,不会污染第 2 轮起的常规规则。
|
||||
# Throttle switch for a regular round (computed by RunScan); Pace_Enabled is this round's value.
|
||||
# RunScanRound restores it each round, then applies --first-no-pace to round 1 only.
|
||||
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)
|
||||
# Throttle duration and file granularity (mutable); defaults below unless --pace/--pace-units given.
|
||||
# RunScan overrides them when given; then --once no longer disables throttling.
|
||||
Pace_Sleep_Ms: int = PACE_SLEEP_MS # --pace <ms>; 0 = no Sleep at either throttle point
|
||||
Pace_File_Units: int = PACE_FILE_UNITS # --pace-units <N>; file-side granularity (dir side stays 16)
|
||||
|
||||
# 节流计数
|
||||
# throttle counters
|
||||
Pace_Dir_Count: int = 0
|
||||
Pace_File_Count: int = 0
|
||||
|
||||
# 进度状态(需求 3)
|
||||
# progress state
|
||||
Prog_Round: int = 0
|
||||
Prog_Roots: int = 0
|
||||
Prog_T0: ULONGLONG = 0
|
||||
@@ -112,16 +75,14 @@ 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 过滤掉的候选总数(含已逐条打印的)
|
||||
# [reject] failure-path counters (reset each round; single-threaded)
|
||||
Reject_Shown: int = 0 # [reject] lines printed this round (cap REJECT_LOG_MAX)
|
||||
Reject_Total: int = 0 # total candidates filtered by PE/.PRVERSN this round
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 扫盘策略小工具
|
||||
# ============================================================
|
||||
# ---- Scan strategy helpers ----
|
||||
|
||||
# 是否跳过该目录名(需求约定的黑名单,大小写无关)
|
||||
# whether to skip this directory name (blacklist, case-insensitive)
|
||||
def IsSkippedDir(Name: str) -> int:
|
||||
if VFs.StrEqNoCase(Name, "WinSxS") != 0:
|
||||
return 1
|
||||
@@ -136,7 +97,7 @@ def IsSkippedDir(Name: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# 文件名是否以 .exe 结尾(大小写无关)
|
||||
# whether the file name ends with .exe (case-insensitive)
|
||||
def HasExeSuffix(Name: str) -> int:
|
||||
n: sizet = string.strlen(Name)
|
||||
if n < 4:
|
||||
@@ -158,12 +119,10 @@ def HasExeSuffix(Name: str) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 进度输出(需求 3;全部走 VLog → stderr;默认安静,仅 --verbose 时输出)
|
||||
# 计数器与节流基线的更新不受开关影响,只有"打印"这一步走 Verbose*。
|
||||
# ============================================================
|
||||
# Progress output (all via VLog -> stderr; quiet unless --verbose).
|
||||
# Counter and throttle-baseline updates are unconditional; only the print step uses Verbose*.
|
||||
|
||||
# 打印一行进度(含 r/dir/file/cand/cur/elapsed),并刷新时间基线
|
||||
# print one progress line (r/dir/file/cand/cur/elapsed) and refresh the time baseline
|
||||
def ProgPrint():
|
||||
global Prog_LastTick
|
||||
Now: ULONGLONG = wb.GetTickCount64()
|
||||
@@ -181,14 +140,14 @@ def ProgPrint():
|
||||
VLog.VerboseFmtS2(" cur=%s elapsed=%s\n", Cur, str(Prog_Elapsed))
|
||||
|
||||
|
||||
# 仅在距上次进度输出 >= PROG_INTERVAL_MS 时打印(保证"无输出超过 ~1s"可见)
|
||||
# print only if >= PROG_INTERVAL_MS since the last progress output
|
||||
def ProgMaybePrint():
|
||||
Now: ULONGLONG = wb.GetTickCount64()
|
||||
if Now - Prog_LastTick >= ULONGLONG(PROG_INTERVAL_MS):
|
||||
ProgPrint()
|
||||
|
||||
|
||||
# 轮次开始:复位计数并打印(轮次 / 扫描根数 / 时间戳)
|
||||
# round start: reset counters and print (round / root count / timestamp)
|
||||
def ProgReset(RoundIdx: int, Roots: int):
|
||||
global Prog_Round, Prog_Roots, Prog_T0, Prog_LastTick
|
||||
global Prog_Dirs, Prog_Files, Prog_Cands, Prog_Cur
|
||||
@@ -212,18 +171,18 @@ def ProgRootEnter(Dir: str):
|
||||
def ProgRootLeave(Dir: str, ElapsedMs: sizet):
|
||||
global Prog_Cur
|
||||
VLog.VerboseFmtIS(" [root] 离开 (耗时 %u ms): %s\n", ElapsedMs, Dir)
|
||||
Prog_Cur = None # 该根结束后其路径缓冲即将释放,避免遗留悬空指针
|
||||
Prog_Cur = None # path buffer is freed after this root; avoid a dangling pointer
|
||||
|
||||
|
||||
def PaceDir():
|
||||
global Pace_Dir_Count, Prog_Dirs
|
||||
VGuard.PollIpc() # check 通道轮询点(非阻塞;无通道/无数据时立即返回)
|
||||
VGuard.PollIpc() # check-channel poll point (non-blocking; returns if idle)
|
||||
Pace_Dir_Count += 1
|
||||
Prog_Dirs += 1
|
||||
if Pace_Dir_Count >= PACE_DIR_UNITS: # 目录侧粒度恒为 PACE_DIR_UNITS=16(--pace-units 只作用文件粒度)
|
||||
if Pace_Dir_Count >= PACE_DIR_UNITS: # dir granularity is always 16 (--pace-units affects file side only)
|
||||
Pace_Dir_Count = 0
|
||||
# Pace_Sleep_Ms == 0(--pace 0):不调用 Sleep(0)——它不是"关闭节流",而是让出时间片,
|
||||
# 与本参数的"关闭节流 sleep"语义不符。计数器与下方进度输出照常推进。
|
||||
# Pace_Sleep_Ms == 0 (--pace 0): do not call Sleep(0) -- it yields the time slice, not
|
||||
# "disable throttle sleep". Counters and progress output still advance.
|
||||
if Pace_Enabled != 0 and Pace_Sleep_Ms > 0:
|
||||
wb.Sleep(ULONG(Pace_Sleep_Ms))
|
||||
ProgPrint()
|
||||
@@ -233,21 +192,19 @@ def PaceDir():
|
||||
|
||||
def PaceFile():
|
||||
global Pace_File_Count, Prog_Files
|
||||
VGuard.PollIpc() # check 通道轮询点(非阻塞;无通道/无数据时立即返回)
|
||||
VGuard.PollIpc() # check-channel poll point (non-blocking; returns if idle)
|
||||
Pace_File_Count += 1
|
||||
Prog_Files += 1
|
||||
if Pace_File_Count >= Pace_File_Units: # 文件侧粒度由 --pace-units 决定(缺省 PACE_FILE_UNITS=64)
|
||||
if Pace_File_Count >= Pace_File_Units: # file granularity set by --pace-units (default 64)
|
||||
Pace_File_Count = 0
|
||||
if Pace_Enabled != 0 and Pace_Sleep_Ms > 0: # 理由同 PaceDir(--pace 0 不 Sleep(0))
|
||||
if Pace_Enabled != 0 and Pace_Sleep_Ms > 0: # same rationale as PaceDir (--pace 0 no Sleep(0))
|
||||
wb.Sleep(ULONG(Pace_Sleep_Ms))
|
||||
ProgMaybePrint()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 初始化
|
||||
# ============================================================
|
||||
# ---- Init ----
|
||||
|
||||
# 分配扫盘静态缓冲(进程生命周期内复用)
|
||||
# allocate scan static buffers (reused for process lifetime)
|
||||
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)
|
||||
@@ -264,18 +221,16 @@ def ScanInit() -> int:
|
||||
if VJson.JsonInit() == 0:
|
||||
VLog.LogStr("[VET] FATAL: 扫盘缓冲分配失败\n")
|
||||
return 0
|
||||
# check 通道缓冲/回执池:分配失败只降级(check 只回执失败原因 CHANNEL_UNAVAILABLE、
|
||||
# 每轮重校跳过不剔除任何条目),不影响扫盘本身
|
||||
# check channel buffers/reply pool: on failure degrade only (check replies
|
||||
# CHANNEL_UNAVAILABLE, no revalidation removal); scan itself is unaffected.
|
||||
if VCheck.CheckInit() == 0:
|
||||
VLog.LogStr("警告: check 通道缓冲不可用,check 请求只回执失败原因(CHANNEL_UNAVAILABLE)、每轮不重校已发现项\n")
|
||||
return 1
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 候选处理
|
||||
# ============================================================
|
||||
# ---- Candidate handling ----
|
||||
|
||||
# ----version 前缀探测(带 5s 超时;超时/启动失败 ⇒ 判不通过,不入缓存)
|
||||
# --version prefix probe (5s timeout; timeout/startup failure => fail, not cached)
|
||||
def ProbeVersion(Target: str) -> int:
|
||||
rc: int = VProc.RunVersionProbe(Target, Probe_Buf, sizet(VProc.VERSION_PROBE_DIAG_CAP), 1)
|
||||
if rc == 0:
|
||||
@@ -284,38 +239,23 @@ def ProbeVersion(Target: str) -> int:
|
||||
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
|
||||
# Handle one .exe candidate: PE + .PRVERSN filter, then --version check; on hit add to cache.
|
||||
# Return 1 = new hit, 0 = miss/existing/failure (TryReadPeInfo rc codes: see VPe.vp).
|
||||
def HandleCandidate(ExeAnsi: str) -> int:
|
||||
global Prog_Cands, Round_Added, Reject_Shown, Reject_Total
|
||||
if VCache.CacheFind(ExeAnsi) >= 0:
|
||||
return 0 # 已知:不重复调用 --version
|
||||
return 0 # known: do not call --version again
|
||||
rc: int = VPe.TryReadPeInfo(ExeAnsi, Scr_Ver, VPe.VER_CAP, Scr_Arch, VPe.ARCH_CAP)
|
||||
if rc != 0:
|
||||
# 非 PE 或无 .PRVERSN:逐条打印前 REJECT_LOG_MAX 条,其余在轮末汇总(仅 --verbose)
|
||||
# not PE or no .PRVERSN: log first REJECT_LOG_MAX lines, rest summarized at round end (--verbose)
|
||||
Reject_Total += 1
|
||||
if Reject_Shown < REJECT_LOG_MAX:
|
||||
Reject_Shown += 1
|
||||
VLog.VerboseFmtIS(" [reject] rc=%u %s\n", sizet(rc), ExeAnsi)
|
||||
return 0
|
||||
Prog_Cands += 1
|
||||
# 仅对已过 PE + .PRVERSN 过滤的 exe 调用 --version(这类很少)。
|
||||
# 发起探测前轮询 check 通道(probe 最长可占 5 秒;把排队的 check 先放行,避免它被一次
|
||||
# 探测压后——check 优先级高于扫盘)。
|
||||
# Call --version only for exes passing the PE + .PRVERSN filter (few).
|
||||
# Poll the check channel before probing (a probe can take 5s; check outranks scanning).
|
||||
VGuard.PollIpc()
|
||||
if ProbeVersion(ExeAnsi) == 0:
|
||||
VLog.VerboseFmtS1(" [跳过] PE+PRVERSN 命中但 --version 前缀不符: %s\n", ExeAnsi)
|
||||
@@ -332,22 +272,13 @@ def HandleCandidate(ExeAnsi: str) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 目录树遍历
|
||||
# ============================================================
|
||||
# ---- Directory tree walk ----
|
||||
|
||||
# 递归枚举目录,查找 .exe。Depth 为当前深度(从 1 起,上限 MAX_SCAN_DEPTH)。
|
||||
#
|
||||
# 两遍枚举(减少 FindNextFileA 次数:文件项不再逐个进入本逻辑):
|
||||
# ① `Dir\*` → 只认目录:递归子目录(跳过重解析点与黑名单目录)
|
||||
# ② `Dir\*.exe` → 扩展名交给系统过滤,只认文件:逐个交给 HandleCandidate
|
||||
# 两遍共用同一块模式缓冲与 FIND_DATA 缓冲:先跑完 ① 并 FindClose,再跑 ②;
|
||||
# 递归帧各自持有自己的缓冲,互不干扰。
|
||||
# ② 仍保留 HasExeSuffix 守卫:8.3 短名可能让 `*.exe` 命中长名并非 .exe 的文件,
|
||||
# 后缀判定可把这类误匹配排除掉。
|
||||
# Recursively enumerate a directory for .exe files; Depth starts at 1, capped by MAX_SCAN_DEPTH.
|
||||
# Two passes (dir\* then dir\*.exe) share one buffer; HasExeSuffix guards 8.3 short-name false hits.
|
||||
def ScanTree(DirAnsi: str, Depth: int) -> int:
|
||||
global Prog_Cur
|
||||
Prog_Cur = DirAnsi # 进度行的"当前目录"
|
||||
Prog_Cur = DirAnsi # current directory for the progress line
|
||||
Pat: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
||||
Fd: bytes = _mbuddy.alloc(FIND_DATA_SIZE)
|
||||
if Pat is None or Fd is None:
|
||||
@@ -357,7 +288,7 @@ def ScanTree(DirAnsi: str, Depth: int) -> int:
|
||||
_mbuddy.free(t.CPtr(Fd))
|
||||
return 0
|
||||
|
||||
# ---- ① 目录:枚举子目录并递归 ----
|
||||
# ---- (1) dirs: enumerate subdirectories and recurse ----
|
||||
if VFs.JoinPath(Pat, VFs.PATH_BUF_CAP, DirAnsi, "*") == 0:
|
||||
_mbuddy.free(t.CPtr(Pat))
|
||||
_mbuddy.free(t.CPtr(Fd))
|
||||
@@ -370,20 +301,20 @@ def ScanTree(DirAnsi: str, Depth: int) -> int:
|
||||
Name: str = str(Fd + FD_FILENAME_OFF)
|
||||
if string.strcmp(Name, ".") != 0 and string.strcmp(Name, "..") != 0:
|
||||
if (Attrs & wf.FILE_ATTRIBUTE_DIRECTORY) != 0:
|
||||
# 跳过重解析点(符号链接/挂载点)与黑名单目录,避免成环与噪声
|
||||
# skip reparse points (symlinks/mounts) and blacklisted dirs to avoid cycles/noise
|
||||
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 即将释放)
|
||||
Prog_Cur = DirAnsi # restore after recursion (Child is about to be freed)
|
||||
_mbuddy.free(t.CPtr(Child))
|
||||
if wf.FindNextFileA(hDir, t.CPtr(Fd)) == 0:
|
||||
break
|
||||
wf.FindClose(hDir)
|
||||
|
||||
# ---- ② 文件:只枚举 *.exe(目录名也可能是 *.exe,已在 ① 递归过,此处跳过)----
|
||||
# ---- (2) files: enumerate only *.exe (such dirs were recursed in (1), skip here) ----
|
||||
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:
|
||||
@@ -407,23 +338,16 @@ def ScanTree(DirAnsi: str, Depth: int) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 一轮扫盘
|
||||
# ============================================================
|
||||
# ---- One scan round ----
|
||||
|
||||
# 对"已发现项"做**完整重校**并剔除不合格者,返回剔除数。
|
||||
# 口径(需求 7):文件存在 + PE/.PRVERSN 可解析 + `<exe> --version` 前 5 字符为 Viper
|
||||
# ——即 VCheck.VerifyExecutable(与 check 通道、扫盘候选过滤同一套判定)。
|
||||
# 顺序:排在普通树扫描之前("优先确认已发现项"),逐条探测前轮询 check 通道
|
||||
# (每条最多占 5 秒探测窗口,轮询保证排队的 check 先被受理)。
|
||||
# 代价:每条已发现项都要重新打开/解析/跑一次 --version(这类条目很少,但单条最长 5 秒,
|
||||
# 见 README 的限制说明)。
|
||||
# Fully revalidate discovered entries and remove failures; returns the removed count.
|
||||
# Same rule as VCheck.VerifyExecutable; runs before tree scan, polls check before each probe (<=5s).
|
||||
def RevalidateDiscovered() -> int:
|
||||
removed: int = 0
|
||||
j: int = 0
|
||||
while j < VCache.CacheCount():
|
||||
Exe: str = VCache.CacheExe(j) # 先取用(RemoveAt 会释放该槽)
|
||||
VGuard.PollIpc() # 探测前轮询(check 优先)
|
||||
Exe: str = VCache.CacheExe(j) # fetch first (RemoveAt frees this slot)
|
||||
VGuard.PollIpc() # poll before probe (check first)
|
||||
if VCheck.VerifyExecutable(Exe) == 0:
|
||||
VLog.VerboseFmtS1(" [重校剔除] %s\n", Exe)
|
||||
VCache.RemoveAt(j)
|
||||
@@ -433,28 +357,23 @@ def RevalidateDiscovered() -> int:
|
||||
return removed
|
||||
|
||||
|
||||
# 执行一轮扫盘:轮开始(轮询 check → 重读 cache.json → 完整重校已发现项)
|
||||
# → 遍历常用目录 / 递归查 .exe(其间持续轮询 check)→ 校验缓存存在性 → 轮询 check
|
||||
# → stdout 输出 JSON → 写缓存文件。
|
||||
# Run one scan round: poll check -> reload cache.json -> revalidate -> walk dirs (polling check)
|
||||
# -> validate existence -> poll check -> emit JSON to stdout -> write cache file.
|
||||
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 轮起的常规轮次。
|
||||
# This round's throttle switch: restore the regular value (RunScan computes it) then apply
|
||||
# --first-no-pace for round 1 only, so the special case never leaks into later rounds.
|
||||
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 新入库的命中项不会被紧随其后的重读清掉,本轮就能进入数组并写回文件;
|
||||
# ③ 对已发现项做完整重校并剔除不合格者("优先确认已发现项",排在树扫描之前)。
|
||||
# Round start (order matters): reload cache.json, then handle queued check requests
|
||||
# (after reload so new hits survive), then revalidate discovered entries before tree scan.
|
||||
VCache.ReloadCache(CachePath)
|
||||
VGuard.PollIpc()
|
||||
Revalidated: int = RevalidateDiscovered()
|
||||
@@ -470,18 +389,18 @@ def RunScanRound(CachePath: str, RoundIdx: int) -> int:
|
||||
ScanTree(str(AnsiDir), 1)
|
||||
ProgRootLeave(str(AnsiDir), sizet(wb.GetTickCount64() - R0))
|
||||
else:
|
||||
# 自动表里的静态两条(C:\Viper / D:\Viper)与盘符枚举结果可能本就不存在,
|
||||
# 逐条回显属过程细节,仅在 --verbose 时给出(--path 的显式告警在 VDirs.BuildTable)
|
||||
# Static auto-table entries and drive letters may not exist; logging each is
|
||||
# process detail, given only with --verbose (explicit --path warnings are in VDirs).
|
||||
VLog.VerboseFmtS1(" 跳过(不存在或非目录): %s\n", str(AnsiDir))
|
||||
_mbuddy.free(t.CPtr(AnsiDir))
|
||||
|
||||
# 本轮被 PE/.PRVERSN 过滤掉的候选:前 REJECT_LOG_MAX 条已在 HandleCandidate 逐条打印,
|
||||
# 这里只补一行汇总(不逐条重打)——逐条与汇总均属过程细节,仅 --verbose 时给出
|
||||
# Candidates filtered by PE/.PRVERSN: first REJECT_LOG_MAX were logged in HandleCandidate;
|
||||
# only a summary line is added here (both are process detail, --verbose only).
|
||||
if Reject_Total > REJECT_LOG_MAX:
|
||||
VLog.VerboseFmt2(" [reject] 其余省略…(本轮累计 %u 条,已逐条打印前 %u 条)\n",
|
||||
sizet(Reject_Total), sizet(REJECT_LOG_MAX))
|
||||
|
||||
# 轮结束前的最后一次轮询:此后到写盘之间不再有等待点(check 命中项因此能进入本轮数组)
|
||||
# Final poll before round end: no wait point before writing, so check hits enter this round's array
|
||||
VGuard.PollIpc()
|
||||
removed: int = VCache.ValidateCache()
|
||||
VJson.EmitJson()
|
||||
@@ -489,14 +408,13 @@ def RunScanRound(CachePath: str, RoundIdx: int) -> int:
|
||||
VCache.WriteCacheFile(CachePath, VJson.JsonBuf(), sizet(VJson.JsonLen()))
|
||||
|
||||
T1: ULONGLONG = wb.GetTickCount64()
|
||||
# 轮末一行(默认保留,不含路径):常驻模式下它是"本轮已跑完"的存活信号(与启动摘要同构),
|
||||
# --once 时即结束行;"下轮开始"时间戳属下轮过程细节,仅 --verbose。
|
||||
# "移除"含两部分:完整重校剔除(Revalidated)+ 存在性校验移除(removed)。
|
||||
# Round-end line (kept by default, no paths): liveness signal, or the final line under --once.
|
||||
# "Removed" = revalidation removals (Revalidated) + existence-check removals (removed).
|
||||
VLog.LogFmt3("轮次 %u 结束: 扫描目录 %u 个, 已知条目 %u 项",
|
||||
sizet(RoundIdx), sizet(Dirs), sizet(VCache.CacheCount()))
|
||||
VLog.LogFmt3(", 新增 %u 项, 移除 %u 项, 耗时 %u ms",
|
||||
sizet(Round_Added), sizet(Revalidated + removed), sizet(T1 - T0))
|
||||
# "下轮开始"时间戳属过程细节(仅 --verbose)
|
||||
# next-round-start timestamp is process detail (--verbose only)
|
||||
if VLog.VerboseOn() != 0:
|
||||
VLog.TimeStampAfter(Prog_Stamp, PROG_STAMP_CAP, sizet(VCli.SleepMs()))
|
||||
VLog.LogFmtS1(", 下轮开始: %s", str(Prog_Stamp))
|
||||
@@ -504,26 +422,22 @@ def RunScanRound(CachePath: str, RoundIdx: int) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# scan 子命令:参数已由 App/main.vp(VCli.ParseScanArgs)解析并完成看门狗拉起;
|
||||
# 本函数只做初始化 + 常驻循环(轮间隔 --sleep,缺省 10000;0 = 不 sleep)。
|
||||
# ============================================================
|
||||
# scan subcommand: args already parsed and watchdog started by App/main.vp (VCli.ParseScanArgs).
|
||||
# This function only initializes and runs the resident loop (round interval --sleep, 10000; 0 = no sleep).
|
||||
def RunScan() -> int:
|
||||
global Pace_Enabled, Pace_Configured, Pace_Sleep_Ms, Pace_File_Units
|
||||
CachePath: str = VCli.CachePath()
|
||||
Once: int = VCli.Once()
|
||||
|
||||
# 启动摘要(默认保留,单行):工具名 + 版本 + 模式 + 参数摘要。
|
||||
# 不含缓存路径与其他被处理路径(路径属过程细节,仅 --verbose 时经目录表/进度行给出):
|
||||
# 该行只承担"进程已启动"这一可观测信号,与轮末一行共同构成本模式的存活指示。
|
||||
# Startup summary (kept, one line): tool name + version + mode + param summary, no paths --
|
||||
# it is only a "process started" signal, forming the liveness indicator with the round-end line.
|
||||
VLog.LogFmtS1("VET - Viper Environment Tool %s (scan)", VCli.Version())
|
||||
VLog.LogFmt2(" sleep=%u once=%u", sizet(VCli.SleepMs()), sizet(Once))
|
||||
VLog.LogFmt1(" need-confirm=%u\n", sizet(VCli.NeedConfirm()))
|
||||
|
||||
if VCli.PaceSet() != 0:
|
||||
# 显式给出 --pace 或 --pace-units ⇒ 一律启用节流(--once 下也生效),时长/粒度取命令行值;
|
||||
# 只给其一则另一项保持缺省(--pace 缺省 20ms、--pace-units 缺省 64)——因为 VCli 未给出的那项
|
||||
# 仍返回其缺省常量。本分支先于下面的 --once 判断,故 --once 不再关闭这类显式节流。
|
||||
# Explicit --pace or --pace-units => always enable throttling (even with --once), using
|
||||
# command-line values; this branch precedes the --once check, so --once keeps it on.
|
||||
Pace_Enabled = 1
|
||||
Pace_Configured = 1
|
||||
Pace_Sleep_Ms = VCli.PaceSleepMs()
|
||||
@@ -531,12 +445,12 @@ def RunScan() -> int:
|
||||
VLog.VerboseFmt2("--pace: 已启用扫描节流 sleep=%u units=%u\n",
|
||||
sizet(Pace_Sleep_Ms), sizet(Pace_File_Units))
|
||||
elif Once != 0:
|
||||
# 未显式给出节流参数时的原有行为:--once 只跑一轮,关闭节流 sleep(PaceDir/PaceFile 均不再 Sleep)
|
||||
# No explicit throttle args: --once runs one round with throttle sleep disabled
|
||||
Pace_Enabled = 0
|
||||
Pace_Configured = 0
|
||||
VLog.VerboseStr("--once: 已关闭扫描节流 sleep\n")
|
||||
# --first-no-pace:只改**第 1 轮**的实际开关(每轮开头由 RunScanRound 从 Pace_Configured
|
||||
# 恢复 + 叠加该特例),故这里不写 Pace_Enabled,只提示一行(属过程细节,仅 --verbose)。
|
||||
# --first-no-pace: affects round 1's actual switch only (RunScanRound restores + applies it);
|
||||
# so do not set Pace_Enabled here, just log one line (process detail, --verbose only).
|
||||
if VCli.FirstNoPace() != 0:
|
||||
VLog.VerboseStr("--first-no-pace: 第 1 轮不启用节流 sleep\n")
|
||||
|
||||
@@ -548,12 +462,11 @@ def RunScan() -> int:
|
||||
if rc0 == 0:
|
||||
VLog.VerboseStr(" ✓ 通过\n\n")
|
||||
else:
|
||||
# 表为空 = 本轮无扫描根:属功能性异常,默认仍报一行(不再是静默)
|
||||
# Empty table = no scan roots this round: a functional anomaly, still reported by default
|
||||
VLog.LogStr("警告: 常用目录表为空(本轮无扫描根)\n\n")
|
||||
|
||||
# 缓存载入移到每轮开始(RunScanRound → VCache.ReloadCache):需求是"每轮开始重读
|
||||
# cache.json",故这里不再单独读一次,避免第 1 轮把同一文件解析两遍。
|
||||
# `--sleep` 之前的第一次读盘由第 1 轮的 ReloadCache 承担(--once 也经过第 1 轮)。
|
||||
# Cache load moved to each round start (RunScanRound -> VCache.ReloadCache); no separate
|
||||
# read here, avoiding double parsing in round 1 (--once also goes through round 1).
|
||||
|
||||
Round: int = 0
|
||||
while True:
|
||||
@@ -561,28 +474,15 @@ def RunScan() -> int:
|
||||
RunScanRound(CachePath, Round)
|
||||
if Once != 0:
|
||||
break
|
||||
# --sleep:轮间隔(缺省 10000;0 = 不 sleep,立即开始下一轮)
|
||||
# --sleep: round interval (default 10000; 0 = no sleep, start next round immediately)
|
||||
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"约定。
|
||||
# ============================================================
|
||||
# scan query switch --search-paths: build the searchable dir table, print it line by line to
|
||||
# stderr, return 0. Reuses VDirs.BuildTable; no scanning, no cache, exit code always 0.
|
||||
def RunSearchPathsQuery() -> int:
|
||||
VDirs.BuildTable()
|
||||
Src: str = "自动表"
|
||||
@@ -591,7 +491,7 @@ def RunSearchPathsQuery() -> int:
|
||||
VLog.LogFmtS1("[search] 目录表来源: %s\n", Src)
|
||||
N: int = VDirs.DirCount()
|
||||
if N <= 0:
|
||||
# 空表必须显式说明,避免用户误以为卡住
|
||||
# empty table must be stated explicitly so the user does not think it hung
|
||||
VLog.LogStr("[search] 没有可搜索的路径(常用目录表为空)\n")
|
||||
return 0
|
||||
Ansi: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
||||
@@ -599,7 +499,7 @@ def RunSearchPathsQuery() -> int:
|
||||
VLog.LogStr(" 警告: 查询缓冲分配失败,仅列出路径(不标注是否会被扫盘跳过)\n")
|
||||
for i in range(N):
|
||||
Path: str = VDirs.DirAt(i)
|
||||
Skip: int = 0 # 1=扫盘时会跳过;Ansi 不可用时保持 0(不标注)
|
||||
Skip: int = 0 # 1 = will be skipped when scanning; stays 0 if Ansi unavailable
|
||||
if Ansi is not None:
|
||||
VFs.Utf8ToAnsi(Path, Ansi, VFs.PATH_BUF_CAP)
|
||||
if VFs.PathIsDir(str(Ansi)) == 0:
|
||||
|
||||
+4
-5
@@ -4,12 +4,11 @@ import sys
|
||||
|
||||
|
||||
# ============================================================
|
||||
# VET App 包入口(包标记文件)
|
||||
# VET App package entry (package marker file)
|
||||
#
|
||||
# 作用:标记 App/lib 为包,使 App/main.vp 能以 `import lib.X as X` 形式引入子模块
|
||||
# (与 TransPyV/App/lib/__init__.vp 的包标记约定一致)。
|
||||
# 共享内存池不在此声明:VET 各子模块自持模块级 `_mbuddy`,由 App/main.vp 统一注入;
|
||||
# 其它模块只经各模块公开函数访问,不交叉读写本文件的全局。
|
||||
# Marks App/lib as a package so App/main.vp can import submodules as `import lib.X as X`.
|
||||
# The shared memory pool is not declared here: each VET submodule holds its own module-level
|
||||
# `_mbuddy`, injected by App/main.vp; other modules only use each module's public functions.
|
||||
# ============================================================
|
||||
|
||||
define LIB_PACKAGE_MARKER = 1
|
||||
+24
-31
@@ -20,20 +20,19 @@ import lib.VCli as VCli
|
||||
|
||||
|
||||
# ============================================================
|
||||
# VET - Viper Environment Tool(入口)
|
||||
# VET - Viper Environment Tool (entry point)
|
||||
#
|
||||
# 三种模式(详见 README):
|
||||
# ver : 单 exe 校验(目录表 + .PRVERSN 段 + `--version` 前缀)
|
||||
# scan : 常驻扫盘发现器(轮间隔由 --sleep 指定,缺省 10 秒;stdout 只输出 JSON)
|
||||
# confirm-watch : 内部隐藏子命令(心跳确认看门狗),由 scan + --need-confirm 自动拉起,
|
||||
# 不出现在用法里;只做 sleep→等 stdin 的 {"ok": 1},失败即终止父进程。
|
||||
# Modes (see README): ver = single-exe check (dir table + .PRVERSN section + `--version` prefix);
|
||||
# scan = resident disk scanner (interval via --sleep, default 10s; stdout is JSON only);
|
||||
# confirm-watch = hidden heartbeat watchdog subcommand, auto-spawned by scan + --need-confirm;
|
||||
# it only does sleep then waits on stdin for {"ok": 1}, aborting the parent on failure.
|
||||
#
|
||||
# 本文件只做:控制台代码页 → Log 初始化 → 建主内存池 → 注入各模块 `_mbuddy`
|
||||
# → 初始化 argv → 分派子命令。所有实现见 lib/ 下各模块。
|
||||
# This file only: console code page -> Log init -> main pool -> inject each module's `_mbuddy`
|
||||
# -> init argv -> dispatch subcommand. All implementations live in lib/.
|
||||
# ============================================================
|
||||
|
||||
define CODE_PAGE = 65001
|
||||
define POOL_SIZE = 16777216 # MemBuddy 主内存池 16 MiB
|
||||
define POOL_SIZE = 16777216 # MemBuddy main pool 16 MiB
|
||||
|
||||
|
||||
@t.CExport
|
||||
@@ -43,7 +42,7 @@ def main() -> int:
|
||||
|
||||
VLog.LogInit()
|
||||
|
||||
# MemBuddy 内存池:sys._init_argv 与各模块的池分配都依赖它
|
||||
# MemBuddy pool: sys._init_argv and all module allocations depend on it
|
||||
arena: bytes = stdlib.malloc(POOL_SIZE)
|
||||
if arena is None:
|
||||
VLog.LogStr("[VET] FATAL: 内存池分配失败\n")
|
||||
@@ -53,10 +52,8 @@ def main() -> int:
|
||||
VLog.LogStr("[VET] FATAL: MemBuddy 初始化失败\n")
|
||||
return 1
|
||||
|
||||
# 逐个注入同一池实例(各模块声明模块级 _mbuddy;不使用 varargs 自定义函数)
|
||||
# 注意:声明了 _mbuddy 的模块一个都不能漏——漏注入时该模块的 _mbuddy 指向零值静态存储
|
||||
# (usable == NULL),其全部 alloc 返回 None,功能静默降级(历史现场:VCheck 漏注入 ⇒
|
||||
# "check 缓冲分配失败")。
|
||||
# Inject the same pool into every module that declares _mbuddy; missing one makes its
|
||||
# _mbuddy point at zeroed static storage, so all allocs return None and it silently degrades.
|
||||
VFs._mbuddy = mb
|
||||
VDirs._mbuddy = mb
|
||||
VGuid._mbuddy = mb
|
||||
@@ -74,40 +71,36 @@ def main() -> int:
|
||||
Cmd: str = sys._argv[1]
|
||||
|
||||
if string.strcmp(Cmd, "ver") == 0:
|
||||
# ver 不解析开关(目标路径固定在 _argv[2]):argv 中出现 --verbose 即打开细节日志
|
||||
# ver does not parse flags (target fixed at _argv[2]): --verbose in argv enables detail logs
|
||||
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] 上的这个开关)。
|
||||
# `--search-paths` may omit `scan` and sit in the subcommand slot; it is a pure query
|
||||
# (no scan, no cache IO) with identical semantics. Set here, merged later by ParseScanArgs.
|
||||
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。
|
||||
# Args are parsed here because the watchdog lifecycle spans all of RunScan, so
|
||||
# --need-confirm/--need-confirm-timeout must be known before entering it.
|
||||
if VCli.ParseScanArgs() != 0:
|
||||
return 1
|
||||
# --verbose 生效时机:解析后立即设置,早于"看门狗拉起日志"与"启动摘要",
|
||||
# 之后所有 VLog.Verbose* 调用据此决定是否输出(默认关闭 = 安静)。
|
||||
# --verbose takes effect right after parsing, before watchdog/summary logs, so all
|
||||
# later VLog.Verbose* calls gate on it (default off = quiet).
|
||||
VLog.SetVerbose(VCli.Verbose())
|
||||
# --search-paths:查询开关——建好"会搜索的目录表"后逐行打印(stderr)并直接退出。
|
||||
# 插在 SpawnWatchdog 之前:该查询不扫盘、不读写缓存,也就不该因 --need-confirm 把看门狗
|
||||
# 拉起来(否则会出现"看门狗已在跑、父进程随即退出"的窗口,还得为它做一次无谓的存活校验)。
|
||||
# 该分支也不经过 VScan.ScanInit,故不分配任何扫盘缓冲——建表只用 VDirs 自己的
|
||||
# _mbuddy 分配(VDirs.vp:159/169/174/204),注入已在 main.vp:61 完成。
|
||||
# --search-paths: build the searched dir table, print it line by line to stderr, exit.
|
||||
# Placed before SpawnWatchdog since it needs no scan/cache and must not start the watchdog.
|
||||
if VCli.SearchPaths() != 0:
|
||||
return VScan.RunSearchPathsQuery()
|
||||
# --need-confirm > 0:拉起独立看门狗子进程(与扫盘解耦)。
|
||||
# 失败即致命:用户显式要求了心跳确认,不能静默降级为"无确认"运行。
|
||||
# --need-confirm > 0: spawn a separate watchdog subprocess (decoupled from scanning).
|
||||
# Failure is fatal: an explicit heartbeat request must not silently run unconfirmed.
|
||||
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)务必回收看门狗,杜绝孤儿进程
|
||||
VGuard.StopWatchdog() # Always reap the watchdog on normal exit (incl. --once) to avoid orphans
|
||||
return RcScan
|
||||
# 内部隐藏子命令:心跳确认看门狗(不出现在用法里;由上面的 scan 分支以自身 exe 拉起)
|
||||
# Hidden internal subcommand: heartbeat watchdog (not in usage; spawned by the scan branch)
|
||||
if string.strcmp(Cmd, "confirm-watch") == 0:
|
||||
return VGuard.RunConfirmWatch()
|
||||
VLog.LogFmtS1("错误: 未知子命令 %s\n", Cmd)
|
||||
|
||||
Reference in new issue
Block a user