diff --git a/App/lib/VCache.vp b/App/lib/VCache.vp index 1d45696..0201a1e 100644 --- a/App/lib/VCache.vp +++ b/App/lib/VCache.vp @@ -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) diff --git a/App/lib/VCheck.vp b/App/lib/VCheck.vp index 1d96775..cf94e4b 100644 --- a/App/lib/VCheck.vp +++ b/App/lib/VCheck.vp @@ -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": "", "id": } -# * 看门狗(confirm-watch)用 json 库解析该行:含 "check"(字符串) 且含 "id"(整数) -# ⇒ 判定为 check 请求,原样逐字节转发(含 '\n')到 IPC 管道写端;该行不参与心跳判定。 -# * 父进程(scan)在**非阻塞**轮询点读该管道(见 VGuard.PollIpc),把完整一行交给本模块。 -# * 本模块执行检查并往 **stdout** 写**单行 JSON** 回执: -# 成功 {"check_result": true, "id": , "viper": {…发现项…}} (不含 check_msg) -# 失败 {"check_result": false, "id": , "check_msg": "<原因>"} +# Channel (attached to --need-confirm; present only while the watchdog runs): +# * A caller writes one line to stdin: {"check": "", "id": } +# * 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": , "viper": {..}} (no check_msg) +# fail {"check_result": false, "id": , "check_msg": ""} +# 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" (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 拉起 --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` (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 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": , "viper": {<发现项字典>}} -# viper 字典与既有发现项**同构同序**:id/executable/version/arch/source/displayName; -# source 固定 "check"(本次答复的来源)。已入库的 exe 用其缓存字段(id/version/arch/ -# displayName 不重写、不改动),保证同一 exe 的 id 稳定。 +# Success reply: {"check_result": true, "id": , "viper": {}}. 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": , "check_msg": "<原因>"} +# Failure reply: {"check_result": false, "id": , "check_msg": ""} 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": , "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": , +# "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: diff --git a/App/lib/VCli.vp b/App/lib/VCli.vp index 0743961..42da696 100644 --- a/App/lib/VCli.vp +++ b/App/lib/VCli.vp @@ -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 个文件一个节流点(缺省 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 :文件粒度,取值域 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 : 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 : 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 个文件一个节流点;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 (0 = disable throttling) +Pace_Units: int = DEFAULT_PACE_FILE_UNITS # --pace-units (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 \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 -# --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 --timeout --parent-pid --ipc-write [--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 diff --git a/App/lib/VDirs.vp b/App/lib/VDirs.vp index 854527a..870d2aa 100644 --- a/App/lib/VDirs.vp +++ b/App/lib/VDirs.vp @@ -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]) diff --git a/App/lib/VFs.vp b/App/lib/VFs.vp index f3caf89..d8f7342 100644 --- a/App/lib/VFs.vp +++ b/App/lib/VFs.vp @@ -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 diff --git a/App/lib/VGuard.vp b/App/lib/VGuard.vp index e4f420d..f99e176 100644 --- a/App/lib/VGuard.vp +++ b/App/lib/VGuard.vp @@ -15,395 +15,287 @@ import w32.win32console as wc # ============================================================ -# VGuard - 心跳确认看门狗(--need-confirm <秒>;仅 scan 模式) +# VGuard - heartbeat confirmation watchdog (--need-confirm ; scan mode only) # -# 设计(本轮重构:与扫盘彻底解耦): -# * 父进程(VET.exe scan --need-confirm x --need-confirm-timeout t)在 App/main.vp 中 -# 用 CreateProcessA 拉起"自身"作为**独立看门狗子进程**: -# VET.exe confirm-watch --interval x --timeout t --parent-pid <父 pid> -# 父进程不阻塞等待看门狗;看门狗只做: -# sleep(interval) → stderr 提示 → 在 timeout 内等 stdin 一行 {"ok": 1} -# → 成功续期继续循环;失败(期限到点仍未收到合法 {"ok": 1} / 形状合法但值不对 / -# 读失败 / stdin 无效)则 -# 在 stdout 逐字打印 {"exit":1, "msg": "timeout/ok!=1"}\n -# → TerminateProcess(父进程) → 自身退出码 1。 -# 其中"期限到点"是唯一的超时失败口:形状可疑的行被忽略并继续等待(见下方"快修"小节)。 -# * 扫盘侧完全不参与确认(VScan 已移除 GuardTick 挂钩);stdout 的"只放 JSON"约定不变。 +# Design: +# * The parent (VET.exe scan --need-confirm x --need-confirm-timeout t) spawns itself as an +# independent watchdog child via CreateProcessA in App/main.vp: +# VET.exe confirm-watch --interval x --timeout t --parent-pid +# The parent never blocks on it. The child loops: +# sleep(interval) -> prompt on stderr -> wait up to timeout for one stdin line {"ok": 1} +# on success renew and loop; on failure (deadline reached without a valid {"ok": 1} / +# valid shape but bad value / read error / invalid stdin) it prints +# {"exit":1, "msg": "timeout/ok!=1"}\n to stdout, calls TerminateProcess(parent) and +# exits with code 1. "Deadline reached" is the only timeout failure exit; suspect lines +# are ignored and the loop keeps waiting (see "Quick-fix"). +# * The scan side does not participate in confirmation; stdout stays "JSON only". # -# 本轮改动:确认输入由"阻塞式读一行"改为"非阻塞取数 + deadline 驱动"。 -# * 旧实现的死结:WaitForMultipleObjects 只回答"stdin 有输入可读",但控制台处于 -# 行输入模式时(setconsolemode 文档:ENABLE_LINE_INPUT = ReadFile/ReadConsole -# "读到回车才返回"),随后的 ReadFile 会一直等到回车;于是"半行 + 不回车"把看门狗 -# 挂在读里,timeout 内是否收到合法 {"ok": 1} 无从判定。 -# * 新实现按输入形态分两路,两路共用同一 deadline、同一未消费输入 FIFO、同一行累积、 -# 同一分类(GuardClassifyLine)与同一取值口径(ParseOkOne 未改): -# ① 控制台(GetFileType=FILE_TYPE_CHAR 且 GetConsoleMode 成功): -# 进入前把 ENABLE_LINE_INPUT|ENABLE_ECHO_INPUT 清掉(掩码 GUARD_RAW_CLEAR_MASK; -# 取舍见下方"控制台模式取舍")→ 取记录(需要新数据才取,见"快修"),分两路: -# * 主路径:kernel32!ReadConsoleInputExA + CONSOLE_READ_NOWAIT(动态解析, -# 见下方"加固 1")——输入缓冲为空时立即返回,取数不再依赖"先查数量"; -# * 降级路径(旧系统无该导出):有界等"可读"(上限 min(剩余, GUARD_LEGACY_WAIT_MS)) -# → GetNumberOfConsoleInputEvents → ReadConsoleInputA(残余竞态见"加固 1")。 -# 两路都只认 KEY_EVENT(0x0001) 且 bKeyDown 的字符,过滤后**整批压入 FIFO**,再由 -# FIFO 逐字节喂行缓冲,累积到 Enter('\r' 归一成 '\n')为止 → 恢复进入前的控制台模式。 -# ② 管道 / 重定向 / 其它(非控制台):管道路径先用 PeekNamedPipe 取"当前真正可用的 -# 字节数",只读 min(可用, 缓冲余量);非管道(磁盘文件重定向等)读一块有界字节 -# (<= GUARD_CHUNK_CAP),读到 0 字节即 EOF;该批同样**整批压入 FIFO** 后再逐字节喂 -# 行缓冲;无可读数据时按 min(剩余, GUARD_POLL_MS) 等句柄推进。 -# * 兜底:任何一次等待/读取都以"剩余毫秒"(GuardRemainMs)为上限,到点即判超时; -# 读取只发生在"已确认有数据"之后,不存在"等到下一次按键 / 下一次回车才算完"的无界阻塞点。 +# Non-blocking input, driven by a single deadline (both paths share the same deadline, FIFO, line +# accumulation, classification and value rule; ParseOkOne is unchanged): +# * Console (GetFileType=FILE_TYPE_CHAR and GetConsoleMode succeeds): clear +# ENABLE_LINE_INPUT|ENABLE_ECHO_INPUT on entry (mask GUARD_RAW_CLEAR_MASK; see "Console mode +# tradeoff"). Primary path: ReadConsoleInputExA + CONSOLE_READ_NOWAIT (see "Hardening 1"); +# fallback (export absent): bounded wait -> GetNumberOfConsoleInputEvents -> ReadConsoleInputA. +# Only KEY_EVENT(0x0001) with bKeyDown is accepted; chars are pushed into the FIFO as a batch, +# fed byte-by-byte into the line buffer until Enter ('\r' normalized to '\n'), then the mode +# is restored. +# * Pipe / redirect / other: for pipes peek the truly available byte count and read only +# min(available, buffer); otherwise read one bounded block (<= GUARD_CHUNK_CAP), 0 bytes = EOF. +# The batch is pushed into the FIFO, then fed into the line buffer; when idle, wait on the +# handle for min(remaining, GUARD_POLL_MS). +# * Every wait/read is bounded by the remaining milliseconds (GuardRemainMs); reads happen only +# after data is known available, so no read can block indefinitely. # -# 控制台模式取舍(为什么清 ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT): -# * 清 ENABLE_LINE_INPUT 是"能判定"的前提:文档(setconsolemode)明确 ENABLE_PROCESSED_INPUT -# 只在"ENABLE_LINE_INPUT 也启用"时才会把回车等字符交给系统处理、不进输入缓冲; -# 清掉行输入后,回车以 KEY_EVENT 记录形式到达本进程,我们才能自己判定"一整行"。 -# * 清 ENABLE_ECHO_INPUT 是"消除失效标志":该位只作用于 ReadFile/ReadConsole 的读回显, -# 且文档注明"仅在 ENABLE_LINE_INPUT 同时启用时可用";本模块根本不调用 ReadFile/ReadConsole -# 读控制台,位留着不产生任何效果。代价:按键不回显(用户看不到自己敲的字符), -# 提示行已写明应输入的内容,判定不受影响。 -# * 其余位一律不动,尤其保留 ENABLE_PROCESSED_INPUT:Ctrl+C 语义与旧实现完全一致 -# (文档:该位决定 CTRL+C 是否进入输入缓冲,还是交给处理函数)。 -# * 恢复策略:进入前用 GetConsoleMode 存原模式,成功切换过才置 Guard_RawActive; -# 正常(本行确认成功)/ 失败(期限到点、值不对、读写失败、EOF)/ 父进程退出 -# 三条路径都调 GuardLeaveRaw 写回原模式。Ctrl+C / Ctrl+Break 这条外部终止路径由 -# SetConsoleCtrlHandler 注册的处理例程在进程终止前写回("加固 2"); -# 只剩"被 TerminateProcess 强杀"(父进程 StopWatchdog 回收、心跳失败杀父)不执行 -# 任何用户态代码,仍无法恢复(该控制台输入模式可能残留"已清行输入")。 +# Console mode tradeoff (why clear ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT): +# * Clearing ENABLE_LINE_INPUT is required to detect a full line ourselves: with line input on, +# Enter is consumed by the system and never reaches the input buffer as a KEY_EVENT. +# * Clearing ENABLE_ECHO_INPUT removes a flag that only affects ReadFile/ReadConsole echo; this +# module never calls those, so the bit had no effect. Cost: no keystroke echo (the prompt +# already states the expected input). +# * All other bits stay, notably ENABLE_PROCESSED_INPUT (Ctrl+C semantics unchanged). +# * Restore: the original mode is saved via GetConsoleMode and Guard_RawActive is set only when +# the switch happened; GuardLeaveRaw writes it back on success / failure / parent-exit paths. +# Ctrl+C / Ctrl+Break / close is handled by SetConsoleCtrlHandler (see "Hardening 2"); only +# TerminateProcess cannot restore the mode. # -# 加固 1:控制台取数改用 ReadConsoleInputExA + CONSOLE_READ_NOWAIT(消除竞态) -# * 旧组合的问题:GetNumberOfConsoleInputEvents 报出 N>0 之后再 ReadConsoleInputA, -# 若另一读者在这两步之间取走记录,ReadConsoleInputA 会**等至少一条记录** -# (该函数无超时参数)⇒ deadline 被突破。 -# * 新组合:ReadConsoleInputExA(hIn, buf, n, &read, CONSOLE_READ_NOWAIT) —— 文档明写 -# CONSOLE_READ_NOWAIT "输入缓冲为空也立即返回",取数与"此刻是否有数据"解耦, -# 不存在"查询 → 阻塞读"的两步组合。 -# * 该导出**没有导入库**(Windows 文档 Requirements:Library none),文档要求用 -# LoadLibrary/GetProcAddress 动态链接,DLL 内只有 ReadConsoleInputExA / ReadConsoleInputExW -# 两个名字。故本模块走**动态解析**(方案 a),而非静态 extern def(方案 b): -# - 静态 extern def 的名字必须是 ReadConsoleInputExA(无 ReadConsoleInputEx 这个导出; -# 本机 D:/msys64/mingw64/lib/libkernel32.a 里只有 ExA/ExW 两个导入符号,已用只读扫描确认), -# 且旧系统(缺该导出)会在**加载期**直接失败,整个 exe 起不来——为一次非阻塞取数 -# 付"VET 全面不可用"的代价不可接受; -# - 动态解析只依赖本仓既有声明 wb.GetModuleHandleA / wb.GetProcAddress -# (includes/w32/win32base.vp:66/65)+ 语言既有的函数指针变量间接调用 -# (t.Callable 注解 → i8** 槽;分派 CallGenMixin.vp:959-990、签名构造与间接调用 -# CallGenLeafKit.vp:741-812),**不新增任何链接期符号**,任何系统上都能加载。 -# - 解析失败(老系统无该导出)⇒ 回退到降级路径:有界等"可读" → -# GetNumberOfConsoleInputEvents → ReadConsoleInputA。该路径的残余竞态无法根除 -# (被取走记录时 ReadConsoleInputA 会等下一条记录),故把这一步之前的等待收紧为 -# min(剩余毫秒, GUARD_LEGACY_WAIT_MS),并在剩余为 0 时**不做**这次读。 -# * 只做一次解析(结果记在 Guard_ReadExState),不在每次取数里反复 GetProcAddress。 +# Hardening 1: read via ReadConsoleInputExA + CONSOLE_READ_NOWAIT (removes a race). +# * GetNumberOfConsoleInputEvents -> ReadConsoleInputA has a race: if another reader consumes the +# record between the two calls, ReadConsoleInputA blocks for at least one record (no timeout) +# and breaks the deadline. ReadConsoleInputExA(..., CONSOLE_READ_NOWAIT) returns immediately on +# an empty buffer, decoupling the read from "is data present now". +# * The export has no import library, so it is resolved dynamically via GetModuleHandleA / +# GetProcAddress (no new link-time symbol, loadable on any system). A static extern def would +# fail at load time on systems lacking the export, taking the whole exe down. +# * If resolution fails (older system), fall back to bounded wait -> GetNumberOfConsoleInputEvents +# -> ReadConsoleInputA, with the pre-wait capped at min(remaining, GUARD_LEGACY_WAIT_MS) and no +# read when remaining is 0. +# * Resolution is done once and cached in Guard_ReadExState. # -# 加固 2:SetConsoleCtrlHandler 兜住控制台模式残留(清除行输入后 Ctrl+C 的写回) -# * 现状:Ctrl+C / Ctrl+Break / 关窗时进程由默认处理终止,来不及执行写回 ⇒ 该终端 -# 后续输入行为异常(行输入被清)。注册处理例程后,进程终止前会先回调本模块的 -# GuardCtrlHandler,把进入前的模式写回(幂等)。 -# * 注册时机:GuardEnterRaw 中"确实改过模式"时(Guard_RawActive=1)注册;摘除时机: -# GuardLeaveRaw 在写回模式后立刻 SetConsoleCtrlHandler(handler, FALSE)。注册与 -# "已切换模式"同生命周期,故 Guard_RawActive==0 ⇔ 未注册,不存在泄漏(见"静态论证")。 -# * handler 返回值一律 FALSE(交还默认处理):语义与加固前逐字一致——Ctrl+C/关窗仍由 -# 默认处理终止本进程。若返回 TRUE("已处理")则必须自行 ExitProcess,否则看门狗会继续 -# 跑到下一个 interval、再按心跳失败 TerminateProcess 父进程,把"用户主动 Ctrl+C" -# 变成"杀父";返回 FALSE 既保住原语义,也让 handler 只需一次 SetConsoleMode。 -# * handler 由系统在**新线程**上调用,只读模块级标量 + 一次 SetConsoleMode,不触碰 -# _mbuddy 池 / VLog 缓冲 / 其它模块级缓冲(无并发写,见"静态论证")。 +# Hardening 2: SetConsoleCtrlHandler covers console-mode residue. +# * Ctrl+C / Ctrl+Break / close would otherwise terminate the process before the mode is written +# back. The registered handler GuardCtrlHandler restores the saved mode (idempotent). +# * Registered in GuardEnterRaw only when the mode actually changed (Guard_RawActive=1), removed +# by GuardLeaveRaw right after the write-back; both share one lifetime, so Guard_RawActive==0 +# <=> not registered (no leak). +# * The handler always returns FALSE (default handling preserved: Ctrl+C/close still terminate +# the process). Returning TRUE would require ExitProcess to avoid turning a user Ctrl+C into a +# watchdog timeout that kills the parent. +# * The handler runs on a new system thread and only reads module scalars and calls SetConsoleMode +# once: no pool / VLog buffer access, so no concurrent write. # -# check 请求通道(本轮新增;执行侧见 VCheck.vp): -# * 父进程建一对管道(GuardIpcCreate):读端留父进程(清继承位),写端可继承并把**数值 -# 句柄**经 `--ipc-write <数值>` 写进看门狗命令行;看门狗用 t.CPtr(数值) 还原句柄。 -# * 看门狗在 stdin 上收到一行后先**分类**(VCheck.ParseCheckLine,用仓库 json 库解析): -# 含 "check"(字符串) 与 "id"(整数) ⇒ check 请求:逐字节原样转发(补 '\n')到 IPC 写端, -# 该行**不计入心跳、也不判失败**(期限继续走;写失败只记 stderr); -# 其余 ⇒ 交给 GuardClassifyLine 三态判定(见下方"快修:批量输入与可疑行")。 -# * 父进程在 VScan 的轮询点非阻塞读该管道(PollIpc:PeekNamedPipe 取可用量 → 只读可用量 -# → 按 '\n' 切行 → 交 VCheck.HandleCheckLine 同步执行并回执)。 +# check request channel (execution side: VCheck.vp): +# * The parent creates a pipe pair (GuardIpcCreate): the read end stays in the parent (inherit +# bit cleared); the write end is inheritable and its numeric handle value is passed via +# `--ipc-write `; the child restores it with t.CPtr(value). +# * The child classifies each stdin line first (VCheck.ParseCheckLine): a line with a string +# "check" and an integer "id" is a check request, forwarded byte-for-byte (plus '\n') to the +# IPC write end, not counted as a heartbeat and never a failure; everything else goes to +# GuardClassifyLine. +# * The parent polls the read end without blocking at VScan poll points (PollIpc: PeekNamedPipe +# available bytes -> read only those -> split on '\n' -> VCheck.HandleCheckLine runs +# synchronously and replies). # -# 快修:批量输入与可疑行(两处缺陷的修复;语义变化见本节末尾) -# * 缺陷 1「同批剩余字节被丢弃」:旧 GuardPumpStream 读到一块后逐字节喂行缓冲,**一行凑满 -# 就 return 1**,同批里"第一行之后"的字节(已被 ReadFile 从管道取走)永久丢失 ⇒ stdin -# 一次写入两行以上即出错;GuardPumpConsole 同病(一批 KEY_EVENT 记录里第一行之后的字符 -# 被丢)。修法:新增模块级**未消费输入 FIFO**(Guard_Pend/Guard_PendLen/Guard_PendPos, -# 容量 GUARD_PEND_CAP=128),两路取数共用,每轮三步: -# ① 先 GuardFeedPending():FIFO 里可能已有上一批留下的整行 ⇒ 凑满即返回 1(不发起读取); -# ② 需要新数据时才取一批(读入量同时受 GUARD_CHUNK_CAP / GUARD_REC_MAX 与 GUARD_PEND_CAP -# 约束,保证放得进 FIFO); -# ③ 把该批**全部**字节(控制台为"过滤后的字符")压入 FIFO,再 GuardFeedPending() 并返回 -# 其结果:该批可能自成一行,剩余字节留在 FIFO 由下一次调用继续喂。 -# 返回码语义不变:1=已凑满一行 / 0=无可读或尚未成行 / -1=读失败 / -# GUARD_PUMP_EOF=不可能再有输入。容量推导见 GUARD_PEND_CAP 声明处 -# (= max(GUARD_CHUNK_CAP=64, GUARD_REC_MAX=16) × 2 余量)。 -# * 缺陷 2「判定过严」:旧 GuardWaitConfirm 把"非 check、又非合法 {"ok": 1}"一律 -# GuardWatchFail,于是"管道分片/被撕裂的残行"与"用户真的输错"后果相同(都打 -# {"exit":1,…} + TerminateProcess(父))。修法:新增 GuardClassifyLine(Buf, Len) 三态分类: -# GUARD_LINE_SUSPECT(0) 形状可疑 ⇒ **忽略该行并继续在剩余期限内等待**(stderr 回显, -# 截断到 GUARD_SUSPECT_ECHO_MAX=120 字符;deadline 不变、不重置期限); -# GUARD_LINE_OK(1) ParseOkOne 通过 ⇒ 本行确认成功; -# GUARD_LINE_BADOK(2) 形状通过但 ParseOkOne 不过 ⇒ 致命失败(保留既有 stderr 文案 -# "[confirm] 收到非 {"ok": 1} 的输入: …" 与 GuardWatchFail(hParent, "ok != 1") 路径)。 -# "形状可疑"的判据(三选一即可疑,字节视角):① 去掉前导空格/制表符后首字符不是 '{'; -# ② 整行不含 '}';③ 行长 > GUARD_OK_LINE_MAX(256)(合法 {"ok": 1} 正常不超过 16 字节)。 -# check 行由 ParseCheckLine **先**判(见上),故第 ③ 项不会误伤长 check 行。 -# * 于是"唯一的超时失败口"是"期限到点仍未收到合法 {"ok": 1}";读失败 / EOF / stdin 无效 / -# 父进程退出等路径一律不变;既有 check 例外不变(优先分类转发、不计心跳、不判失败)。 -# * 被忽略的行**不会绕过期限**:两条忽略路径(check 转发 / 可疑行)都落到循环里的 -# "剩余为 0 ⇒ 超时失败"检查上(不 reset deadline)⇒ 持续涌入的可疑行或 check 行 -# 也会在期限到点收尾,"期限到点才判失败"对"无输入"与"只有被忽略的行"一致成立。 -# * 两处取舍(写在这里便于推翻): -# ① "形状合法但值不对"({"ok": 0} / {"ok": 2})**仍判致命失败**——"用户真的输错"这条 -# 保持原语义不放宽; -# ② **空行**由旧语义的"立即失败(stdin 输入为空行)"改为"按可疑行忽略"(首字符不是 '{'), -# 最终由期限到点失败 ⇒ stderr 原因由"stdin 输入为空行"变为"超时无输入",失败时机也 -# 从"收到空行"推迟到"期限到点"。这是本轮**可接受的语义变化**。 +# Quick-fix: batch input and suspect lines. +# * Fix 1 (dropped bytes in a batch): the old pump returned as soon as one line completed, losing +# the rest of the batch already read from the pipe. Now an unconsumed-input FIFO +# (Guard_Pend/Guard_PendLen/Guard_PendPos, capacity GUARD_PEND_CAP=128) is shared by both +# paths, three steps per round: (1) feed the FIFO first (it may already hold a full line); +# (2) read a new batch only when needed (bounded by GUARD_CHUNK_CAP/GUARD_REC_MAX/GUARD_PEND_CAP); +# (3) push the whole batch into the FIFO, then feed it. Return codes: 1=line complete, +# 0=no data / not yet a line, -1=read error, GUARD_PUMP_EOF=no more input possible. +# Invariant: GUARD_PEND_CAP >= GUARD_CHUNK_CAP and >= GUARD_REC_MAX. +# * Fix 2 (over-strict verdict): GuardClassifyLine(Buf, Len) returns SUSPECT(0) = ignore the line +# and keep waiting within the deadline (stderr echo, truncated to GUARD_SUSPECT_ECHO_MAX), +# OK(1) = ParseOkOne passed, BADOK(2) = valid shape but bad value (fatal failure, existing +# stderr text and GuardWatchFail(hParent, "ok != 1") path kept). +# A line is suspect if any of: (1) first char after leading space/tab is not '{'; (2) it contains +# no '}'; (3) it is longer than GUARD_OK_LINE_MAX(256). check lines are classified first, so (3) +# does not affect them. +# * The only timeout failure exit is "deadline reached without a valid {"ok": 1}"; ignored lines +# (check forwards / suspect lines) never reset the deadline, so a stream of them still ends at +# the deadline. +# * Accepted behavior change: an empty line is now ignored (suspect) instead of failing at once; +# the failure then occurs at the deadline with reason "timeout, no input". # -# 为什么用子进程而不是线程(取舍): -# * 本仓有 includes/vthreading.vp,但线程与主流程共享同一地址空间: -# - VLog.Log_Buffer(VET/App/lib/VLog.vp:26)、VScan 的模块级进度/静态缓冲 -# (Scr_*/Prog_*,VScan.vp:60-88)都是进程级单例,线程并发写会互相踩踏, -# 需要为"日志/格式化"再加锁并让出 MemBuddy 分配路径; -# - MemBuddy(memhub.MemBuddy)池非线程安全,看门狗的分配会与扫盘分配竞争; -# - TerminateProcess 无法用于"只杀主线程",线程方案下"失败即终止整个程序" -# 要靠 ExitProcess/abort,且要保证不写缓存、不再打一轮 JSON,反而更绕。 -# * 子进程天然内存隔离:句柄表、MemBuddy 池、模块级缓冲互不干扰,失败即 -# TerminateProcess(父进程不做任何收尾 ⇒ 不写缓存、不再输出本轮数组 JSON), -# 与旧 sys.exit(1) 的"立即终止"语义一致且更强。 -# * 代价:多一个进程与一次 CreateProcessA;可被"父进程句柄"精确回收(见下)。 +# Why a subprocess instead of a thread: +# * Threads share the address space: VLog/VScan module buffers and the MemBuddy pool are +# process-wide singletons and not thread-safe; TerminateProcess cannot kill one thread. +# * A subprocess is memory-isolated (handle table, MemBuddy pool, module buffers); failure means +# TerminateProcess(parent) with no cleanup, matching the old sys.exit(1) "terminate now". +# * Cost: one extra process and one CreateProcessA; reclaimed precisely via the parent handle. # -# 生命周期与"无孤儿"保证: -# * 父正常结束(含 --once)/参数解析后提前返回:main.vp 调 StopWatchdog() → -# TerminateProcess(看门狗) + CloseHandle。 -# * 父异常退出(Ctrl+C / 被 TerminateProcess):看门狗持有父进程句柄(OpenProcess), -# 父进程对象一终止即置信号 ⇒ WaitForSingleObject / WaitForMultipleObjects 立即返回, -# 看门狗自行退出(不需要父侧配合)。 -# * 看门狗自身异常/崩溃:父进程的 Watch_Process 句柄仍可被 StopWatchdog 关闭回收。 +# Lifecycle / no orphans: +# * Normal parent exit: StopWatchdog() -> TerminateProcess(child) + CloseHandle. +# * Parent killed (Ctrl+C / TerminateProcess): the child holds an OpenProcess handle to the +# parent, so its wait returns immediately and the child exits on its own. +# * Child crash: the parent's Watch_Process handle is still closed by StopWatchdog. # -# 依赖本仓既有声明(未改 includes/): -# wf.GetStdHandle(includes/w32/win32file.vp:120)、wf.ReadFile(:90)、wf.GetFileType(:98)、 -# wf.SetHandleInformation(:140)、wf.HANDLE_FLAG_INHERIT(:136)、 -# wf.CreatePipe(:139)(check 通道管道;与 VProc.vp:121 同一用法)、 -# wf.GetModuleFileNameA(:133)、wf.STD_INPUT_HANDLE(:44)、wf.STD_OUTPUT_HANDLE(:45)、 -# wf.STD_ERROR_HANDLE(:46)、 -# wsync.WaitForSingleObject(includes/w32/win32sync.vp:37)、 -# wsync.WaitForMultipleObjects(includes/w32/win32sync.vp:39)、 -# wp.CreateProcessA(includes/w32/win32process.vp:88)、wp.TerminateProcess(:90)、 -# wp.GetExitCodeProcess(:91)、wp.OpenProcess(:93)、wp.GetCurrentProcessId(:95)、 -# wp.STARTF_USESTDHANDLES(:27)、 -# wp.STARTUPINFOA(:42)、wp.PROCESS_INFORMATION(:82)、 -# wb.WAIT_OBJECT_0(:15)、wb.WAIT_TIMEOUT(:16)、wb.WAIT_FAILED(:14)、 -# wb.INVALID_HANDLE_VALUE(:9)、wb.CloseHandle(:64)、wb.GetLastError(:62)、 -# wb.GetTickCount64(:80)、wb.Sleep(:77)、 -# wc.GetConsoleMode(includes/w32/win32console.vp:98)、wc.SetConsoleMode(:99)、 -# wc.GetNumberOfConsoleInputEvents(:102)、wc.ReadConsoleInputA(:100)(仅降级路径)、 -# wc.ENABLE_LINE_INPUT(:6)、wc.ENABLE_ECHO_INPUT(:7)、wc.KEY_EVENT(:29)、 -# wb.GetModuleHandleA(includes/w32/win32base.vp:66)、wb.GetProcAddress(:65) -# (加固 1 的动态解析:只取函数地址,不新增链接期符号)、 -# VPe.ReadU16(VET/App/lib/VPe.vp:82)、VPe.ReadU32(:88)(PE 小端读取,按字节偏移复用)、 -# sys.exit(includes/sys.vp:26)、memhub.MemBuddy.alloc(includes/memhub.vp:725)、 -# VCheck.ParseCheckLine / VCheck.HandleCheckLine(VET/App/lib/VCheck.vp:请求行分类与执行)、 -# VLog 原语(VET/App/lib/VLog.vp)、VCli.ParseUint/ParseUint64/ParseWatchArgs(VET/App/lib/VCli.vp)。 -# -# 本模块自声明(本仓 includes 未声明;prototype + DLL 见"API 证据表"): -# SYNCHRONIZE: winnt.h 0x00100000(kernel32!OpenProcess 的访问权限位)。 -# FILE_TYPE_DISK/CHAR/PIPE: fileapi.h 0x0001/0x0002/0x0003(kernel32!GetFileType 返回值)。 -# GUARD_RAW_CLEAR_MASK: ~(ENABLE_LINE_INPUT|ENABLE_ECHO_INPUT) = ~0x0006 = 0xFFFFFFF9(wincon.h)。 -# CONSOLE_READ_NOWAIT: 0x0002(ReadConsoleInputEx 的 wFlags;文档明确"输入缓冲为空也立即返回"; -# 同族的 CONSOLE_READ_NOREMOVE = 0x0001 本轮不用)。 -# CTRL_C_EVENT/CTRL_BREAK_EVENT/CTRL_CLOSE_EVENT/CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT: -# 0/1/2/5/6(consoleapi.h;处理例程的入参取值。本模块的例程对所有取值一律写回模式后返回 -# FALSE,故只在此记录常量值,不在代码里分支)。 -# kernel32!PeekNamedPipe(原型见下方 extern def;本仓 includes 无此声明)。 -# kernel32!SetConsoleCtrlHandler(原型见下方 extern def;本仓 includes 无此声明)。 -# kernel32!ReadConsoleInputExA:**无导入库、无头文件**,只能动态解析(本模块不声明 extern, -# 见"加固 1"与 GuardResolveReadEx)。 +# Dependencies: existing declarations in this repo (w32 file/sync/process/console/base, +# VPe.ReadU16/ReadU32, memhub.MemBuddy.alloc, VCheck.ParseCheckLine/HandleCheckLine, VLog, +# VCli.ParseUint/ParseUint64/ParseWatchArgs, sys.exit) plus the self-declared items below: +# SYNCHRONIZE (0x00100000), FILE_TYPE_DISK/CHAR/PIPE (0x0001/0x0002/0x0003), +# GUARD_RAW_CLEAR_MASK (~0x0006 = 0xFFFFFFF9), CONSOLE_READ_NOWAIT (0x0002), +# CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN_EVENT (0/1/2/5/6, not branched on), +# kernel32!PeekNamedPipe and kernel32!SetConsoleCtrlHandler (prototypes in the extern defs), +# kernel32!ReadConsoleInputExA (no import library; resolved dynamically, see "Hardening 1"). # ============================================================ define SYNCHRONIZE = 0x00100000 # winnt.h -# OpenProcess 需要的访问权限:PROCESS_TERMINATE(0x0001) | SYNCHRONIZE(0x00100000) +# Access rights for OpenProcess: PROCESS_TERMINATE(0x0001) | SYNCHRONIZE(0x00100000) define WATCH_PARENT_ACCESS = 0x00100001 -define CONFIRM_WATCH_CMD = "confirm-watch" # 内部隐藏子命令名 -# stdin 行缓冲:既要放 {"ok": 1},也要放 check 请求行({"check":"","id":}, -# 路径可达 MAX_PATH 量级)⇒ 取 4096(与 VCheck.CK_LINE_MAX 同值)。行超此上限按"一行结束" -# 处理(与旧实现"缓冲写满即截断成一行"一致;超长的 check 行会因路径被截断而判 NOT_FOUND)。 +define CONFIRM_WATCH_CMD = "confirm-watch" # internal hidden subcommand name +# stdin line buffer: holds both {"ok": 1} and a check request line ({"check":"","id":N}), +# path up to MAX_PATH. 4096 (same as VCheck.CK_LINE_MAX); a longer line is treated as one line end. define GUARD_LINE_CAP = 4096 -define GUARD_STAMP_CAP = 64 # 时间戳文本缓冲 -define SELF_PATH_CAP = 1024 # GetModuleFileNameA 输出缓冲 -# 命令行额外量(子命令 + interval/timeout/parent-pid/ipc-write + --verbose + NUL;句柄按 -# 最多 20 位十进制字符计。留足余量以免 snprintf 截断出"看似正常实则错参"的命令行) +define GUARD_STAMP_CAP = 64 # timestamp text buffer +define SELF_PATH_CAP = 1024 # GetModuleFileNameA output buffer +# Extra command-line room (subcommand + interval/timeout/parent-pid/ipc-write + --verbose + NUL; +# handles counted as up to 20 decimal digits). Enough slack to avoid a truncated command line. define WATCH_CMD_EXTRA_CAP = 256 -define WATCH_HANDLE_COUNT = 2 # WaitForMultipleObjects 句柄个数 -# 拉起后"存活校验"的等待窗口(毫秒)。代价:每次 scan 启动多等这么一会儿(README 已注明); -# 取 400 是"够看门狗跑完参数解析/OpenProcess/首次等待"与"不显著拖慢启动"之间的折中。 +define WATCH_HANDLE_COUNT = 2 # number of handles passed to WaitForMultipleObjects +# Liveness-probe wait window after spawn (ms). Cost: each scan start waits this long. +# 400 balances "enough for the child to parse args/OpenProcess/first wait" and startup speed. define WATCH_ALIVE_PROBE_MS = 400 -# ---- 非阻塞确认输入(控制台 / 管道 / 重定向)---- -# GetFileType 返回值(fileapi.h;本仓 include 未声明这几个常量) -define FILE_TYPE_DISK = 0x0001 # 磁盘文件(含 `< file` 重定向) -define FILE_TYPE_CHAR = 0x0002 # 字符设备(控制台/LPT 等) -define FILE_TYPE_PIPE = 0x0003 # 管道/套接字(含 `|` 重定向) -# 输入形态(GuardInputKind 返回值) -define GUARD_IN_CONSOLE = 1 # 控制台(CHAR + GetConsoleMode 成功) -define GUARD_IN_PIPE = 2 # 管道(可用 PeekNamedPipe 预询可读量) -define GUARD_IN_STREAM = 3 # 其它流(磁盘文件等:只能有界读) -# 清位掩码:ENABLE_LINE_INPUT(0x0002)|ENABLE_ECHO_INPUT(0x0004) 的补(wincon.h) +# ---- non-blocking confirmation input (console / pipe / redirect) ---- +# GetFileType return values (fileapi.h; not declared in this repo's includes) +define FILE_TYPE_DISK = 0x0001 # disk file (incl. '< file' redirect) +define FILE_TYPE_CHAR = 0x0002 # character device (console/LPT etc.) +define FILE_TYPE_PIPE = 0x0003 # pipe/socket (incl. '|' redirect) +# input kind (GuardInputKind return values) +define GUARD_IN_CONSOLE = 1 # console (CHAR + GetConsoleMode succeeds) +define GUARD_IN_PIPE = 2 # pipe (PeekNamedPipe can pre-query readable bytes) +define GUARD_IN_STREAM = 3 # other stream (disk file etc.: bounded read only) +# clear mask: complement of ENABLE_LINE_INPUT(0x0002)|ENABLE_ECHO_INPUT(0x0004) (wincon.h) define GUARD_RAW_CLEAR_MASK = 0xFFFFFFF9 -# INPUT_RECORD 内 KEY_EVENT_RECORD 字段偏移(x64 布局推导见 VET/README.md「控制台记录的读取口径」) +# KEY_EVENT_RECORD field offsets in INPUT_RECORD (x64 layout; see VET/README.md) define GUARD_KEY_DOWN_OFF = 4 # KeyEvent.bKeyDown (BOOL) -define GUARD_KEY_CHAR_OFF = 14 # KeyEvent.uChar (WCHAR/CHAR union 头 2 字节) +define GUARD_KEY_CHAR_OFF = 14 # KeyEvent.uChar (first 2 bytes of the WCHAR/CHAR union) define GUARD_REC_SIZE = 20 # sizeof(INPUT_RECORD)=EventType(2)+pad(2)+union(16) -define GUARD_REC_MAX = 16 # 单次 ReadConsoleInputA 最多读的记录数 -define GUARD_REC_CAP = 320 # 记录缓冲字节数(20*16) -define GUARD_CHUNK_CAP = 64 # 管道/文件单次 ReadFile 上限(字节) -define GUARD_POLL_MS = 20 # 无可读数据时的推进节拍(同 VScan.PACE_SLEEP_MS) -# 取数结果码(两路共用;-1 为"读取失败") -define GUARD_PUMP_EOF = -2 # 不可能再出现输入(ReadFile 读到 0 字节) +define GUARD_REC_MAX = 16 # max records per ReadConsoleInputA call +define GUARD_REC_CAP = 320 # record buffer size in bytes (20*16) +define GUARD_CHUNK_CAP = 64 # max bytes per ReadFile for pipe/file +define GUARD_POLL_MS = 20 # idle tick with no readable data (same as VScan.PACE_SLEEP_MS) +# pump result codes (shared; -1 means read error) +define GUARD_PUMP_EOF = -2 # no more input possible (ReadFile returned 0 bytes) -# ---- 快修:未消费输入 FIFO(两路取数共用;见模块头「快修:批量输入与可疑行」)---- -# 容量的推导(常量实际值取自本文件上方声明): -# * 管道/文件路径单批入队量 = min(管道可用量, GUARD_CHUNK_CAP=64, GUARD_PEND_CAP) <= 64; -# * 控制台路径单批入队量 = 单次取的记录条数 <= GUARD_REC_MAX=16,且每条记录至多产出 1 个字节; -# * 故"单批入队量"的全局上界 = max(64, 16) = 64;再取 2 倍余量 => 128。 -# 为什么 2 倍余量就够(不必按 GUARD_LINE_CAP=4096 取):GuardPump* 只在"需要新数据"时才读, -# 而"需要新数据"的唯一条件是 GuardFeedPending() 返回 0(该返回只可能出现在 FIFO 被耗尽之后), -# 即"入队时 FIFO 必为空";此时 FIFO 只需容纳一个整批。余量用于吸收"未来把批上限调大"的改动。 -# 不变量(改动任一常量时须保持):GUARD_PEND_CAP >= GUARD_CHUNK_CAP,且 >= GUARD_REC_MAX -# (否则单批读入量放不进 FIFO;GuardPumpStream/GuardPumpConsole 里的两处夹取是该不变量的 -# 显式执行,GuardPendPut 的边界守卫是最后一道防线)。 -define GUARD_PEND_CAP = 128 # 未消费输入 FIFO 容量(字节) -# 可疑行回显的截断长度(stderr 只回显前缀,避免把超长输入整行打出去) +# ---- unconsumed-input FIFO (shared by both paths; see module head "Quick-fix") ---- +# Capacity 128 = max(GUARD_CHUNK_CAP=64, GUARD_REC_MAX=16) x 2. Invariant: keep GUARD_PEND_CAP +# >= GUARD_CHUNK_CAP and >= GUARD_REC_MAX so one batch fits (clamps in the pumps enforce it). +define GUARD_PEND_CAP = 128 # unconsumed-input FIFO capacity (bytes) +# suspect-line echo truncation length (stderr echoes only the prefix) define GUARD_SUSPECT_ECHO_MAX = 120 -# 行分类结果码(GuardClassifyLine 返回值,见模块头「快修:批量输入与可疑行」) -define GUARD_LINE_SUSPECT = 0 # 形状可疑 ⇒ 忽略该行并继续等(期限不变) -define GUARD_LINE_OK = 1 # 合法 {"ok": 1} ⇒ 本行确认成功 -define GUARD_LINE_BADOK = 2 # 形状合法但值不对 ⇒ 致命失败(口径不放宽) -# 形状检查的长度上界:合法 {"ok": 1} 正常不超过 16 字节(去掉空格/制表符后更短), -# 取 256 留足余量(可容忍 "{ \"ok\" : 1 }" 之类的空白展开),超过即按"被撕裂的残行/噪声"处理。 +# line classification codes (GuardClassifyLine; see module head "Quick-fix") +define GUARD_LINE_SUSPECT = 0 # suspect shape => ignore the line and keep waiting +define GUARD_LINE_OK = 1 # valid {"ok": 1} => this line confirms success +define GUARD_LINE_BADOK = 2 # valid shape but bad value => fatal failure (rule not relaxed) +# Shape-check length limit: a valid {"ok": 1} is normally <= 16 bytes; 256 leaves room for +# whitespace expansion like "{ \"ok\" : 1 }". Longer lines are treated as torn/noise. define GUARD_OK_LINE_MAX = 256 -# ---- check 请求通道(看门狗 → 父进程的 IPC 管道;依附 --need-confirm)---- -# 行缓冲上限与看门狗侧 GUARD_LINE_CAP 同值(请求行由看门狗逐字节转发,长度不会超过它) +# ---- check request channel (watchdog -> parent IPC pipe; attached to --need-confirm) ---- +# line buffer cap, same as the watchdog's GUARD_LINE_CAP (lines are forwarded byte-for-byte) define IPC_LINE_CAP = 4096 -define IPC_READ_CHUNK = 1024 # 单次 ReadFile 上限(只读"已确认可用"的部分) -define IPC_PIPE_SIZE = 65536 # CreatePipe 的 nSize 提示(默认过小,给足以免看门狗写阻塞) +define IPC_READ_CHUNK = 1024 # max bytes per ReadFile (only the confirmed-available part) +define IPC_PIPE_SIZE = 65536 # CreatePipe nSize hint (default too small; keep the write from blocking) -# ---- 加固 1:ReadConsoleInputExA 动态解析(无导入库,只能 GetProcAddress)---- -define CONSOLE_READ_NOWAIT = 0x0002 # 输入缓冲为空也立即返回(文档 wFlags) -# ReadConsoleInputExA 的解析状态(Guard_ReadExState;0 = 未尝试解析) -define GUARD_EX_OK = 1 # 已解析到函数地址(走主路径) -define GUARD_EX_MISSING = 2 # 该导出不存在(走降级路径,不再重试) -# 降级路径"等可读"的单次上限(毫秒):把这一步的等待收紧,避免一次等待吃掉整个剩余预算; -# 取 5 是"够等一次输入到达"与"不显著拖慢判定"之间的折中(其后还有 GUARD_POLL_MS 节拍)。 +# ---- Hardening 1: dynamic resolution of ReadConsoleInputExA (no import library) ---- +define CONSOLE_READ_NOWAIT = 0x0002 # returns immediately on an empty input buffer (documented wFlags) +# resolution state for ReadConsoleInputExA (Guard_ReadExState; 0 = not tried) +define GUARD_EX_OK = 1 # resolved to a function address (primary path) +define GUARD_EX_MISSING = 2 # export absent (fallback path, no retry) +# Single wait cap (ms) for the fallback "wait readable": keep this step from eating the whole +# remaining budget. 5 balances "enough for one input to arrive" and promptness. define GUARD_LEGACY_WAIT_MS = 5 -# SetConsoleCtrlHandler 的 Add 参数(consoleapi.h 的 TRUE/FALSE 同名,此处用字面量以免混淆) +# Add parameter for SetConsoleCtrlHandler (literal to avoid confusion with TRUE/FALSE) define GUARD_CTRL_ADD = 1 define GUARD_CTRL_REMOVE = 0 -# Win32 自声明(本仓 includes/w32 无 PeekNamedPipe): -# kernel32!PeekNamedPipe: BOOL PeekNamedPipe(HANDLE hNamedPipe, LPVOID lpBuffer, -# DWORD nBufferSize, LPDWORD lpBytesRead, LPDWORD lpTotalBytesAvail, -# LPDWORD lpBytesLeftThisMessage); -# 原型出处:namedpipeapi.h / Kernel32.lib / kernel32.dll(project.vpj 已链接 kernel32)。 -# 语义:句柄必须是管道读端;不取走数据、立即返回;lpTotalBytesAvail = 当前可读字节数。 +# kernel32!PeekNamedPipe (not declared in this repo's includes): BOOL PeekNamedPipe(HANDLE, +# LPVOID, DWORD, LPDWORD, LPDWORD, LPDWORD). Handle must be a pipe read end; peeks without +# consuming and returns immediately; lpTotalBytesAvail = bytes currently readable. extern def PeekNamedPipe(hNamedPipe: wb.HANDLE, lpBuffer: t.CPtr, nBufferSize: ULONG, lpBytesRead: t.CPtr, lpTotalBytesAvail: ULONG*, lpBytesLeftThisMessage: t.CPtr) -> BOOL -# Win32 自声明(本仓 includes/w32 无 SetConsoleCtrlHandler): -# kernel32!SetConsoleCtrlHandler: BOOL SetConsoleCtrlHandler(PHANDLER_ROUTINE HandlerRoutine, -# BOOL Add); -# typedef BOOL (WINAPI *PHANDLER_ROUTINE)(DWORD dwCtrlType); // consoleapi.h:114-115 -# 原型出处:consoleapi.h / Kernel32.lib / kernel32.dll(本机 libkernel32.a 内含该导入符号)。 -# 第一个形参按本仓"函数指针/void* 用 t.CPtr 传"的写法声明;实参直接写处理例程的函数名, -# 由 TPV 把函数引用 bitcast 成函数指针(先例:includes/vthreading.vp:22-27 把函数名传给 -# CreateThread 的 VOIDPTR 形参;IR 现场 Test/BuddyTest/.tpv_build/temp/8e/ -# 8efc91cbab0452f0.ll:2672-2676 的 `bitcast i32 (i8*)* @... to i8*` + `call @CreateThread`)。 -# 返回 0 = 失败(GetLastError 取原因);Add=FALSE 时摘除。 +# kernel32!SetConsoleCtrlHandler (not declared in this repo's includes): BOOL SetConsoleCtrlHandler( +# PHANDLER_ROUTINE, BOOL Add), where PHANDLER_ROUTINE is BOOL (WINAPI*)(DWORD dwCtrlType). +# The handler name is passed directly and TPV bitcasts the function reference to a pointer. +# Returns 0 on failure (GetLastError); Add=FALSE removes the handler. extern def SetConsoleCtrlHandler(HandlerRoutine: t.CPtr, Add: BOOL) -> BOOL -# 由 App/main.vp 注入(与其它模块同一 MemBuddy 实例) +# injected by App/main.vp (same MemBuddy instance as other modules) _mbuddy: memhub.MemBuddy* -# 看门狗句柄(父进程侧;StopWatchdog 负责关闭) -Watch_Process: wb.HANDLE = None # CreateProcessA → PROCESS_INFORMATION.hProcess +# watchdog handles (parent side; StopWatchdog closes them) +Watch_Process: wb.HANDLE = None # CreateProcessA -> PROCESS_INFORMATION.hProcess Watch_Thread: wb.HANDLE = None # PROCESS_INFORMATION.hThread -# 看门狗侧缓冲(confirm-watch 内一次性分配) -Guard_Line: bytes = None # stdin 行缓冲 -Guard_Stamp: bytes = None # "当前时间"文本缓冲 -Guard_Dl: bytes = None # "本轮期限"文本缓冲 -Guard_Recs: bytes = None # 控制台 INPUT_RECORD 数组缓冲(原生字节,按偏移读) -Guard_Chunk: bytes = None # 管道/文件读取块缓冲 -# 未消费输入 FIFO(快修;两路取数共用):读取批次先整批压入这里,再逐字节喂 Guard_Line。 -# 语义:[Guard_PendPos, Guard_PendLen) 为"尚未喂给行缓冲"的字节区间;Pos==Len 表示空。 -Guard_Pend: bytes = None # 未消费输入 FIFO(容量 GUARD_PEND_CAP) -Guard_PendLen: int = 0 # FIFO 已写入的字节数(写入端游标) -Guard_PendPos: int = 0 # FIFO 已喂出的字节数(消费端游标) -Guard_Fill: int = 0 # 本轮已累积的行长度(Guard_Line 内,不含 NUL) -Guard_SavedMode: ULONG = 0 # 进入"原始按键"模式前的控制台模式 -Guard_RawActive: int = 0 # 1=当前处于已切换模式(退出前必须写回) +# watchdog-side buffers (allocated once inside confirm-watch) +Guard_Line: bytes = None # stdin line buffer +Guard_Stamp: bytes = None # "current time" text buffer +Guard_Dl: bytes = None # "round deadline" text buffer +Guard_Recs: bytes = None # console INPUT_RECORD array buffer (raw bytes, read by offset) +Guard_Chunk: bytes = None # pipe/file read chunk buffer +# Unconsumed-input FIFO (shared by both paths): a read batch is pushed here, then fed to Guard_Line. +# [Guard_PendPos, Guard_PendLen) is the range not yet fed; Pos==Len means empty. +Guard_Pend: bytes = None # unconsumed-input FIFO (capacity GUARD_PEND_CAP) +Guard_PendLen: int = 0 # bytes written into the FIFO (write cursor) +Guard_PendPos: int = 0 # bytes fed out of the FIFO (consume cursor) +Guard_Fill: int = 0 # current accumulated line length (in Guard_Line, excluding NUL) +Guard_SavedMode: ULONG = 0 # console mode before entering raw-key mode +Guard_RawActive: int = 0 # 1 = currently in switched mode (must be written back before exit) -# 看门狗侧:check 通道的管道写端(由父进程经 `--ipc-write <数值>` 传入;None=无通道)。 -# 只用于"原样转发 check 请求行",写失败仅记 stderr(不改变心跳语义)。 +# Watchdog-side write end of the check channel (passed via `--ipc-write `; None = no channel). +# Only forwards check request lines verbatim; a write failure is logged to stderr only. Guard_IpcWrite: wb.HANDLE = None -# ---- check 请求通道(父进程侧;非阻塞轮询)---- -# 管道由父进程(本模块)建立:读端留在父进程(清掉继承位),写端设为可继承并把**数值句柄** -# 经命令行 `--ipc-write <数值>` 交给看门狗(看门狗用 t.CPtr(数值) 得到句柄直接 WriteFile)。 -Ipc_Read: wb.HANDLE = None # 管道读端(父进程持有;None=未建立/已关闭) -Ipc_Line: bytes = None # 行累积缓冲(IPC_LINE_CAP) -Ipc_Chunk: bytes = None # 单次读取块(IPC_READ_CHUNK) -Ipc_Fill: int = 0 # 已累积的字节数(不含 NUL) +# ---- check request channel (parent side; non-blocking poll) ---- +# The pipe is created by the parent: the read end stays here (inherit bit cleared); the write end +# is inheritable and its numeric handle is passed to the watchdog via `--ipc-write `. +Ipc_Read: wb.HANDLE = None # pipe read end (parent-held; None = not created/closed) +Ipc_Line: bytes = None # line accumulation buffer (IPC_LINE_CAP) +Ipc_Chunk: bytes = None # single read chunk (IPC_READ_CHUNK) +Ipc_Fill: int = 0 # accumulated byte count (excluding NUL) -# 加固 1:ReadConsoleInputExA 的函数指针槽(t.Callable 注解 → i8** 槽,调用即间接 call; -# 形参表按 ABI 记:HANDLE/PINPUT_RECORD/LPDWORD 都是 i8*,DWORD=i32,wFlags=USHORT=i16)。 -# 值由 GuardResolveReadEx 一次性填入;Guard_ReadExState 取值为 0/GUARD_EX_OK/GUARD_EX_MISSING。 +# Hardening 1: function pointer slot for ReadConsoleInputExA (t.Callable -> i8** slot, indirect call). +# ABI: HANDLE/PINPUT_RECORD/LPDWORD are i8*, DWORD=i32, wFlags=USHORT=i16. Filled once by +# GuardResolveReadEx; Guard_ReadExState is 0 / GUARD_EX_OK / GUARD_EX_MISSING. Guard_ReadEx: t.Callable[[t.CPtr, t.CPtr, ULONG, t.CPtr, USHORT], BOOL] = None Guard_ReadExState: int = 0 -# 加固 2:控制事件处理例程的注册状态与"已切换模式"的控制台句柄 -# (两者都在 GuardCtrlHandler 所在的系统线程里被读取,故只在主线程注册前写入一次) -Guard_ConHandle: wb.HANDLE = None # GuardEnterRaw 改过模式的那个控制台句柄 -Guard_CtrlHandlerOn: int = 0 # 1=已注册(GuardLeaveRaw 负责摘除) +# Hardening 2: control-handler registration state and the console handle whose mode was switched. +# Both are read on the system thread running GuardCtrlHandler, so they are written once before registration. +Guard_ConHandle: wb.HANDLE = None # console handle whose mode GuardEnterRaw switched +Guard_CtrlHandlerOn: int = 0 # 1 = registered (GuardLeaveRaw removes it) -# WaitForMultipleObjects 需要一个"句柄数组"(HANDLE 即 VOIDPTR,x64 下 8 字节)。 -# 用两个 HANDLE 字段的类型化结构体,按声明顺序连续存放 = 16 字节,恰为 Win32 期望。 +# WaitForMultipleObjects needs a handle array (HANDLE is VOIDPTR, 8 bytes on x64). This typed +# struct of two HANDLE fields is stored contiguously (16 bytes), exactly as Win32 expects. class WATCH_HANDLES(): H0: wb.HANDLE H1: wb.HANDLE # ============================================================ -# check 请求通道:IPC 管道(父进程侧:建立 / 关闭 / 非阻塞轮询) +# check request channel: IPC pipe (parent side: create / close / non-blocking poll) # -# 为什么用"管道 + 数值句柄经命令行传递": -# * 父子之间已有 stdin/stdout/stderr 三个共享句柄,但它们都被"用户输入 / 结果 JSON / -# 日志"占用(stdout 的契约是"只放单行 JSON",不能被请求流污染),故另开一对管道; -# * 管道由父进程 CreatePipe 建立:读端留在父进程(SetHandleInformation 清掉继承位), -# 写端保持可继承(bInheritHandle=1)并随 CreateProcessA(bInheritHandles=1) 进入看门狗; -# 看门狗侧无需知道管道任何信息,只需 "句柄数值" ⇒ 经命令行 `--ipc-write <数值>` 传入; -# * 传"数值"而不是"复制/命名管道":句柄值本身就是 pid 一样的小整数,子进程用 -# t.CPtr(数值)(即 ptrtoint/inttoptr 的逆运算)即可还原为可用的 HANDLE。 -# 句柄值按 u64 位模式传递(ParseUint64 严格解析),因此内核句柄那种"高位为 1 的 -# 符号扩展值"也能原样还原,不做 32 位截断。 -# * 父进程在 CreateProcessA 成功后**立即关掉自己的写端副本**:此后本进程再拉起 -# `--version` 子进程时不会把这个写端一起继承出去(读端本就不可继承)。 +# Dedicated pipe (stdin/stdout/stderr are already taken; stdout's contract is single-line JSON). +# Created by the parent: read end stays here (inherit bit cleared), write end stays inheritable and +# its numeric handle is passed by value via `--ipc-write ` (u64 bit pattern, so high-bit +# handles survive; the child restores it with t.CPtr). The parent closes its write-end copy right +# after CreateProcessA so later --version probes do not inherit it. # -# 父进程侧读取**绝不阻塞**: -# * 先 PeekNamedPipe 取"当前真正可用的字节数";可用为 0 就直接返回(不调用 ReadFile); -# * 只读 min(可用, IPC_READ_CHUNK) 字节——ReadFile 在"确有数据可读"时立即返回; -# * 每次轮询的数据量上界 = 进入时管道中已有的字节数(排空后即返回),无任何"等下一次到来", -# 故轮询点可以放在扫盘的高频路径上(PaceDir/PaceFile)而不会拖慢/挂住扫盘。 -# -# 行切分:与看门狗侧同一口径——'\n' 结束一行(看门狗把行原样转发时补的那个 '\n'); -# 行内容不含 '\n'(看门狗侧 GuardFeedByte 已把 '\n' 排除在缓冲之外)。 -# 超长行(≥ IPC_LINE_CAP-1 仍未见到 '\n')按"丢弃该行"处理(Ipc_Fill 归零), -# 既不越界也不把两行粘成一行。 +# Reads never block: peek the currently available byte count, return if 0, else read only +# min(available, IPC_READ_CHUNK); the per-poll bound is what was already in the pipe, so the poll +# can sit on the scan hot path. '\n' ends a line (content excludes it); an overlong line is dropped. # ============================================================ -# 建立 IPC 管道:成功返回**写端句柄**(由调用方交给看门狗并在用完后 CloseHandle), -# 失败返回 None(调用方按"看门狗拉起失败=致命"处理)。读端存入 Ipc_Read。 +# Create the IPC pipe. Returns the write-end handle on success (caller passes it to the watchdog and +# closes it later); None on failure (treated as fatal). The read end is stored in Ipc_Read. def GuardIpcCreate() -> wb.HANDLE: global Ipc_Read, Ipc_Line, Ipc_Chunk, Ipc_Fill - GuardIpcClose() # 幂等:如已有旧管道先关掉 + GuardIpcClose() # idempotent: close any old pipe first if Ipc_Line is None: Ipc_Line = _mbuddy.alloc(IPC_LINE_CAP) if Ipc_Chunk is None: @@ -413,7 +305,7 @@ def GuardIpcCreate() -> wb.HANDLE: Sa: wb.SECURITY_ATTRIBUTES = wb.SECURITY_ATTRIBUTES() Sa.nLength = uint32(wb.SECURITY_ATTRIBUTES.__sizeof__()) Sa.lpSecurityDescriptor = t.CPtr(0) - Sa.bInheritHandle = 1 # 两端先都建为可继承,随后单方面清读端 + Sa.bInheritHandle = 1 # create both inheritable first, then clear only the read end hRead: wb.HANDLE = None hWrite: wb.HANDLE = None if wf.CreatePipe(&hRead, &hWrite, @@ -421,7 +313,7 @@ def GuardIpcCreate() -> wb.HANDLE: VLog.LogFmt1("[confirm] 警告: check 通道 CreatePipe 失败 (GetLastError=%u)\n", sizet(wb.GetLastError())) return None - # 读端只在父进程用:清掉继承位,避免被任何子进程(看门狗、--version 探测)继承 + # read end is parent-only: clear inherit bit so no child (watchdog, --version probe) inherits it wf.SetHandleInformation(hRead, wf.HANDLE_FLAG_INHERIT, 0) wf.SetHandleInformation(hWrite, wf.HANDLE_FLAG_INHERIT, wf.HANDLE_FLAG_INHERIT) Ipc_Read = hRead @@ -429,7 +321,7 @@ def GuardIpcCreate() -> wb.HANDLE: return hWrite -# 关闭 IPC 读端(幂等;StopWatchdog 与 SpawnWatchdog 的失败路径都会调)。 +# Close the IPC read end (idempotent; called by StopWatchdog and SpawnWatchdog failure paths). def GuardIpcClose(): global Ipc_Read, Ipc_Fill if Ipc_Read is not None: @@ -438,9 +330,9 @@ def GuardIpcClose(): Ipc_Fill = 0 -# 非阻塞轮询 IPC 读端:把"当前已可用"的字节全部取走并按 '\n' 切行,逐行交给 VCheck 处理 -# (处理是同步的:check 的"读 .PRVERSN → --version → 回执"整段在本次调用内完成)。 -# 返回本次已处理的 check 请求行数(0 = 无数据/非 check 行/通道未建立)。 +# Non-blocking poll of the IPC read end: drain currently available bytes, split on '\n', hand each +# line to VCheck (synchronously: PRVERSN read -> --version -> reply all inside this call). +# Returns the number of check request lines handled (0 = no data / non-check line / no channel). def PollIpc() -> int: global Ipc_Fill if Ipc_Read is None or Ipc_Read == wb.INVALID_HANDLE_VALUE: @@ -449,12 +341,12 @@ def PollIpc() -> int: while True: Avail: ULONG = 0 if PeekNamedPipe(Ipc_Read, t.CPtr(0), 0, t.CPtr(0), &Avail, t.CPtr(0)) == 0: - return Done # 管道异常:本轮不再读(不阻塞、不重试) + return Done # pipe error: stop reading this round (no block, no retry) if Avail == 0: - return Done # 无数据 ⇒ 立即返回(轮询点的高频路径) + return Done # no data => return immediately (frequent poll path) N: ULONG = ULONG(IPC_READ_CHUNK) if Avail < N: - N = Avail # 只读"确已有数据"的部分 ⇒ ReadFile 不会阻塞 + N = Avail # read only the confirmed-available part => ReadFile will not block Br: ULONG = 0 if wf.ReadFile(Ipc_Read, Ipc_Chunk, N, &Br, None) == 0: return Done @@ -465,7 +357,7 @@ def PollIpc() -> int: while i < int(Br): Ch: int = int(ub[i]) i += 1 - if Ch == 10: # '\n':一行结束 + if Ch == 10: # '\n': end of a line Ipc_Line[Ipc_Fill] = 0 if Ipc_Fill > 0: if VCheck.HandleCheckLine(str(Ipc_Line)) != 0: @@ -473,17 +365,17 @@ def PollIpc() -> int: Ipc_Fill = 0 else: if Ipc_Fill >= IPC_LINE_CAP - 1: - Ipc_Fill = 0 # 超长行:丢弃该行(不越界、不与下一行粘连) + Ipc_Fill = 0 # overlong line: drop it (no overflow, no merging) else: Ipc_Line[Ipc_Fill] = char(Ch) Ipc_Fill += 1 # ============================================================ -# 父进程侧:拉起 / 停止看门狗 +# parent side: spawn / stop the watchdog # ============================================================ -# 取自身 exe 全路径(GetModuleFileNameA(NULL,...))。返回 1=成功(Out 已 NUL 结尾),0=失败。 +# Get own exe full path (GetModuleFileNameA(NULL,...)). Returns 1 = success (Out NUL-terminated), 0 = failure. def SelfExePath(Out: bytes, Cap: int) -> int: if Out is None or Cap < 2: return 0 @@ -491,33 +383,20 @@ def SelfExePath(Out: bytes, Cap: int) -> int: if n == 0: return 0 if sizet(n) >= sizet(Cap): - return 0 # 缓冲不足(路径被截断)⇒ 视为失败 + return 0 # buffer too small (path truncated) => treat as failure Out[int(n)] = 0 return 1 -# 拉起看门狗子进程。返回 1=已拉起,0=失败。 -# IntervalSec:--need-confirm 的秒数(>0,调用方保证);TimeoutMs:确认等待超时(毫秒)。 -# 说明: -# * lpApplicationName=NULL,命令行首 token 为自身全路径(与 VProc 的 CreateProcessA 用法一致)。 -# * stdout/stderr/stdin 三个标准句柄都显式指向父进程当前句柄(STARTF_USESTDHANDLES), -# 因此看门狗的 JSON 与日志与父进程落同一处(控制台或重定向),不新建管道、不复制输出。 -# * dwCreationFlags=0:让子进程共享父进程控制台(不新开窗口),保证控制台输入句柄在子进程内 -# 仍可读、且 Ctrl+C 能同时送达(双保险:另有父进程句柄监控)。 -# * 拉起成功后做一次**存活校验**(WaitForSingleObject(hProcess, WATCH_ALIVE_PROBE_MS)): -# CreateProcessA 返回非 0 只说明进程对象建起来了,不代表看门狗真在跑——它可能因 -# 自身参数/环境问题立刻退出(历史现场:--parent-pid 被截断成 -1 判非法 → exit 1), -# 那样 --need-confirm 会静默失效(README 的"拉起失败=致命"落空)。 -# WAIT_OBJECT_0(已退出)⇒ 取退出码打 stderr + stdout 打唯一一行失败 JSON + 关句柄 -# ⇒ 返回 0(main.vp 的既有失败分支据此中止 scan)。WAIT_TIMEOUT(仍在跑)⇒ 视为存活, -# 继续。其余等待返回值(如 WAIT_FAILED)信息不足,按存活处理(宁可少报也不误杀 scan)。 -# 时序:CreateProcessA → 探一次(<=400ms) → 存活则打"已启动"日志并继续扫盘 / -# 已退出则打诊断 + 失败 JSON 并返回 0(致命)。 +# Spawn the watchdog child. Returns 1 = spawned, 0 = failure. IntervalSec > 0 (caller guarantees); +# TimeoutMs = confirmation timeout (ms). Shares the parent's standard handles (STARTF_USESTDHANDLES) +# and console (dwCreationFlags=0). A liveness probe after spawn treats an immediate child exit +# (WAIT_OBJECT_0) as fatal (exit code + failure JSON, return 0); other wait results count as alive. def SpawnWatchdog(IntervalSec: int, TimeoutMs: int) -> int: global Watch_Process, Watch_Thread if IntervalSec <= 0: - return 1 # 关闭:不拉起(调用方仅在 >0 时调用),视为"无需看门狗" - StopWatchdog() # 幂等:如已有旧看门狗先回收 + return 1 # disabled: do not spawn (caller only calls when >0) + StopWatchdog() # idempotent: reclaim any old watchdog first Self: bytes = _mbuddy.alloc(SELF_PATH_CAP) Cmd: bytes = _mbuddy.alloc(SELF_PATH_CAP + WATCH_CMD_EXTRA_CAP) @@ -534,9 +413,8 @@ def SpawnWatchdog(IntervalSec: int, TimeoutMs: int) -> int: VLog.LogStr("[confirm] 警告: GetModuleFileNameA 取自身路径失败,看门狗未启动\n") return 0 - # check 请求通道:先建管道(写端数值要写进命令行,必须在 CreateProcessA 之前拿到)。 - # 失败即致命(与看门狗拉起失败同一处理):用户显式要求心跳确认,其附带的 check 通道 - # 若静默缺失,等于悄悄少了一项被请求的能力。 + # Build the pipe first (the write-end value must be in the command line before CreateProcessA). + # Failure is fatal: silently lacking the requested check channel drops an explicit capability. IpcW: wb.HANDLE = GuardIpcCreate() if IpcW is None: _mbuddy.free(t.CPtr(Self)) @@ -544,10 +422,9 @@ def SpawnWatchdog(IntervalSec: int, TimeoutMs: int) -> int: VLog.LogStr("[confirm] 警告: check 通道管道建立失败,看门狗未启动\n") return 0 - # 命令行首 token 为自身全路径(引号包裹,兼容路径含空格); - # 父进程为 --verbose 时为子进程追加同名开关,使看门狗按同一详细度规则输出。 - # --ipc-write <数值>:check 请求通道的管道**写端句柄数值**(父进程按 u64 位模式给出, - # 看门狗用 t.CPtr(数值) 还原句柄)。缺该参数(如手工调用)时看门狗侧按"无通道"处理。 + # First token is the self path (quoted for spaces); --verbose is forwarded so the child uses the + # same verbosity. --ipc-write is the pipe write-end handle value (u64), restored by the + # child with t.CPtr; a missing argument means "no channel". Pid: ULONG = wp.GetCurrentProcessId() Extra: str = "" if VCli.Verbose() != 0: @@ -558,7 +435,7 @@ def SpawnWatchdog(IntervalSec: int, TimeoutMs: int) -> int: sizet(IntervalSec), sizet(TimeoutMs), sizet(Pid), uint64(IpcW), Extra) - # 标准句柄:与父进程同一(继承控制台/重定向);stdin 也一并交给看门狗 + # Standard handles: same as the parent (inherited console/redirect); stdin is also handed over hIn: wb.HANDLE = wf.GetStdHandle(wf.STD_INPUT_HANDLE) hOut: wb.HANDLE = wf.GetStdHandle(wf.STD_OUTPUT_HANDLE) hErr: wb.HANDLE = wf.GetStdHandle(wf.STD_ERROR_HANDLE) @@ -569,7 +446,7 @@ def SpawnWatchdog(IntervalSec: int, TimeoutMs: int) -> int: _mbuddy.free(t.CPtr(Cmd)) VLog.LogStr("[confirm] 警告: stdin 句柄无效,看门狗未启动(心跳确认需要 stdin)\n") return 0 - # 尽力把标准句柄标记为可继承(控制台句柄通常本就随控制台继承;失败忽略) + # Best-effort mark standard handles inheritable (console handles usually inherit anyway; ignore) wf.SetHandleInformation(hIn, wf.HANDLE_FLAG_INHERIT, wf.HANDLE_FLAG_INHERIT) if hOut is not None and hOut != wb.INVALID_HANDLE_VALUE: wf.SetHandleInformation(hOut, wf.HANDLE_FLAG_INHERIT, wf.HANDLE_FLAG_INHERIT) @@ -586,7 +463,7 @@ def SpawnWatchdog(IntervalSec: int, TimeoutMs: int) -> int: Ok: BOOL = wp.CreateProcessA(t.CPtr(0), Cmd, t.CPtr(0), t.CPtr(0), 1, 0, t.CPtr(0), t.CPtr(0), &Si, &Pi) - CreateErr: ULONG = wb.GetLastError() # 紧跟调用读取(free 会覆盖 GetLastError) + CreateErr: ULONG = wb.GetLastError() # read right after the call (free overwrites GetLastError) _mbuddy.free(t.CPtr(Self)) _mbuddy.free(t.CPtr(Cmd)) if Ok == 0: @@ -596,30 +473,29 @@ def SpawnWatchdog(IntervalSec: int, TimeoutMs: int) -> int: sizet(CreateErr)) return 0 - # 写端已由看门狗继承 ⇒ 父进程立即关掉自己的副本:这样后续任何 CreateProcessA - # (--version 探测)都不会把这个写端再继承出去。 + # The write end was inherited by the watchdog => close our copy now, so later CreateProcess calls + # (--version probe) do not inherit it again. wb.CloseHandle(IpcW) Watch_Process = Pi.hProcess Watch_Thread = Pi.hThread - # 存活校验(详见函数头注释):只把 WAIT_OBJECT_0(子进程已退出)判为致命。 + # Liveness probe (see function head): only WAIT_OBJECT_0 (child exited) is fatal. Wp: ULONG = wsync.WaitForSingleObject(Watch_Process, WATCH_ALIVE_PROBE_MS) if Wp == wb.WAIT_OBJECT_0: Ec: ULONG = 0 wp.GetExitCodeProcess(Watch_Process, &Ec) VLog.LogFmt1("[confirm] 看门狗拉起后立即退出 (exit=%u),心跳确认无法生效\n", sizet(Ec)) - # 致命路径与 StopWatchdog 一样各自 CloseHandle 一次,并清空模块级句柄 - # (避免随后 StopWatchdog/再次 Spawn 造成双重关闭)。 + # Like StopWatchdog, close each handle once and clear the module handles (avoid a later + # StopWatchdog / re-Spawn double-closing). wb.CloseHandle(Watch_Process) Watch_Process = None wb.CloseHandle(Watch_Thread) Watch_Thread = None - GuardIpcClose() # 看门狗已退出 ⇒ check 通道随之作废,读端一并关闭 - # 与看门狗自身失败路径(GuardWatchFail)逐字节相同的那一行:格式仍是单行 JSON。 - # 保留同一 msg(而非新造 "watchdog exited"):消费方按 "exit"==1 判中止, - # 且 README 已把这行标为看门狗失败的唯一 stdout 约定;精确原因走上一条 stderr。 + GuardIpcClose() # watchdog exited => check channel is void, close the read end too + # Byte-identical to the watchdog failure line (GuardWatchFail), still single-line JSON. Keep + # the same msg because consumers abort on "exit"==1; the precise reason goes to stderr above. VLog.StdoutWrite("{\"exit\":1, \"msg\": \"timeout/ok!=1\"}\n") return 0 VLog.LogFmt2("[confirm] 心跳看门狗已启动 (pid=%u, 每 %u 秒一次", @@ -628,9 +504,8 @@ def SpawnWatchdog(IntervalSec: int, TimeoutMs: int) -> int: return 1 -# 停止看门狗(幂等;父进程正常结束/提前返回时调用)。 -# TerminateProcess 立即置"终止"标记,子进程不会再执行用户态代码;再 WaitForSingleObject -# 有界等待其真正退出,确保父进程结束前无遗留;最后关闭两个句柄与 check 通道的管道读端。 +# Stop the watchdog (idempotent; called on normal/early parent exit). TerminateProcess marks it +# terminated (no more user-mode code); a bounded wait then ensures it is gone before closing handles. def StopWatchdog(): global Watch_Process, Watch_Thread if Watch_Process is not None: @@ -645,14 +520,12 @@ def StopWatchdog(): # ============================================================ -# 输入解析(沿用原判定口径) +# input parsing # ============================================================ -# 严格判定"存在 ok 且其值为 1"(容忍 key/值与冒号之间的空格 32 与制表 9)。 -# 结构约束:`"ok"` 前一个非空白字符必须是 '{'(123) 或 ','(44)(或缓冲区起点)—— -# 避免把某个字符串值里的 "ok" 误当成键;值必须是数字 1(49) 且其后不得紧跟数字 -# (拒绝 12/01)。返回 1=ok==1;其它一切情况(ok:0 / 空行 / 非 JSON / ok 缺失 / -# 值带引号 / 非数字 / 负数)返回 0。 +# Strictly match "key ok with value 1" (tolerating space 32 / tab 9 around key and colon). The char +# before '"ok"' must be '{'(123), ','(44) or the buffer start; the value must be digit 1(49) not +# followed by a digit. Returns 1 only for ok==1, else 0. def ParseOkOne(Buf: bytes, Len: int) -> int: if Buf is None or Len <= 0: return 0 @@ -670,10 +543,10 @@ def ParseOkOne(Buf: bytes, Len: int) -> int: KeyPos = 1 else: pk: int = int(ub[k]) - if pk == 123 or pk == 44: # '{' 或 ',' + if pk == 123 or pk == 44: # '{' or ',' KeyPos = 1 if KeyPos != 0 and i + 3 < Len: - # 依次为 'o'(111) 'k'(107) '"'(34) + # then 'o'(111) 'k'(107) '"'(34) if int(ub[i + 1]) == 111 and int(ub[i + 2]) == 107 and int(ub[i + 3]) == 34: j: int = i + 4 while j < Len and (int(ub[j]) == 32 or int(ub[j]) == 9): @@ -686,51 +559,44 @@ def ParseOkOne(Buf: bytes, Len: int) -> int: if j + 1 >= Len: return 1 nx: int = int(ub[j + 1]) - if nx < 48 or nx > 57: # 后随非数字 ⇒ 值恰为 1 + if nx < 48 or nx > 57: # next char not a digit => value is exactly 1 return 1 return 0 -# 行分类(快修;判定顺序见模块头「快修:批量输入与可疑行」)。返回值: -# GUARD_LINE_SUSPECT(0) = 形状可疑 ⇒ 忽略该行、继续在剩余期限内等待(不判失败); -# GUARD_LINE_OK(1) = 合法 {"ok": 1} ⇒ 本行确认成功; -# GUARD_LINE_BADOK(2) = 形状合法但值不对(如 {"ok": 0} / {"ok": 2})⇒ 致命失败。 -# 顺序:先做形状检查(三项任一不满足即"可疑"),形状通过后才用既有的 ParseOkOne 判值—— -# ParseOkOne 的口径一字未改,本函数只负责"值不对"与"形状可疑"的分流。 -# 三项形状判据(字节视角,容忍前导空格 32 / 制表 9): -# ① 去掉前导空白后的首字符不是 '{'(123)(含空行、全空白行); -# ② 整行不含 '}'(125)(被撕裂/截断的残行); -# ③ 行长超过 GUARD_OK_LINE_MAX(256)(明显异常)。 -# 注意分类的调用位置:check 请求行由 VCheck.ParseCheckLine **先**判(该行优先转发,且长 check 行 -# 可远超 256 字节),故第 ③ 项不会误伤 check 行。 +# Line classification (see module head "Quick-fix"). Returns SUSPECT(0) = ignore and keep waiting, +# OK(1) = valid {"ok": 1} (success), BADOK(2) = valid shape but bad value (fatal failure). Shape is +# checked first; ParseOkOne (unchanged) decides the value only after shape passes. +# Suspect if, in byte terms: (1) first char after leading space/tab is not '{'(123); (2) no '}'(125) +# in the line; (3) length > GUARD_OK_LINE_MAX(256). check lines are classified first, so (3) is safe. def GuardClassifyLine(Buf: bytes, Len: int) -> int: if Buf is None or Len <= 0: - return GUARD_LINE_SUSPECT # ①(空行/无内容) + return GUARD_LINE_SUSPECT # (1) empty line / no content if Len > GUARD_OK_LINE_MAX: - return GUARD_LINE_SUSPECT # ③ + return GUARD_LINE_SUSPECT # (3) ub: BYTEPTR = BYTEPTR(Buf) i: int = 0 while i < Len and (int(ub[i]) == 32 or int(ub[i]) == 9): i += 1 if i >= Len: - return GUARD_LINE_SUSPECT # ①(全空白行) + return GUARD_LINE_SUSPECT # (1) all-blank line if int(ub[i]) != 123: # '{' - return GUARD_LINE_SUSPECT # ① + return GUARD_LINE_SUSPECT # (1) j: int = i while j < Len: if int(ub[j]) == 125: # '}' break j += 1 if j >= Len: - return GUARD_LINE_SUSPECT # ② + return GUARD_LINE_SUSPECT # (2) if ParseOkOne(Buf, Len) != 0: return GUARD_LINE_OK return GUARD_LINE_BADOK -# 把当前这一行(Guard_Line / Guard_Fill)逐字节转发到 check 通道的 IPC 写端,并补一个 '\n' -# 作为行分隔(父进程按 '\n' 切行)。行内容与用户输入逐字节相同(含空格与大小写)。 -# 写失败/无通道只记 stderr:该行本就不参与心跳判定,**不改变心跳语义**(期限继续走)。 +# Forward the current line (Guard_Line / Guard_Fill) byte-for-byte to the IPC write end plus '\n' +# (the parent splits on '\n'). Content matches user input exactly. A write failure / no channel is +# stderr-only and does not change heartbeat semantics. def GuardForwardLine(): if Guard_IpcWrite is None or Guard_IpcWrite == wb.INVALID_HANDLE_VALUE: VLog.LogStr("[confirm] 警告: 收到 check 请求但无 check 通道(未传 --ipc-write),已丢弃该行\n") @@ -746,12 +612,11 @@ def GuardForwardLine(): # ============================================================ -# 非阻塞取数(控制台 / 管道 / 重定向两路共用同一 deadline 与同一行累积) +# non-blocking input pump (console / pipe / redirect share one deadline and line accumulation) # ============================================================ -# 取剩余毫秒:GetTickCount64 差值,已过期饱和到 0(调用方据此判超时,不出现负值)。 -# 上界依据:--timeout 上界 CONFIRM_TIMEOUT_MAX_MS = 86400000(VCli.vp:36 校验), -# 故 Deadline - Now < 2^32,转 ULONG 不截断。 +# Remaining milliseconds: GetTickCount64 difference, saturated to 0 when expired (never negative). +# Since the --timeout cap (86400000) is < 2^32, the ULONG cast cannot truncate. def GuardRemainMs(Deadline: ULONGLONG) -> ULONG: Now: ULONGLONG = wb.GetTickCount64() if Now >= Deadline: @@ -759,15 +624,12 @@ def GuardRemainMs(Deadline: ULONGLONG) -> ULONG: return ULONG(Deadline - Now) -# 把一个字节累积进行缓冲(两路唯一的累积点)。返回 1=已凑满一行,0=尚未成行。 -# 行结束口径与旧 ReadConfirmLine 逐条一致: -# * '\n'(10) 结束一行且不入缓冲,其前紧邻的 '\r'(13) 去掉(重定向输入的 CRLF); -# * 缓冲写满(GUARD_LINE_CAP-1)按一行结束处理(旧实现同样返回截断行); -# * 每次结束/追加后写 NUL,Guard_Fill 为本行内容长度(供 ParseOkOne 与日志使用)。 -# 控制台路径把 Enter 的 '\r' 归一成 '\n' 再送入本函数,故两种输入形态共用同一口径。 +# Accumulate one byte (the only accumulation point). Returns 1 = a line completed, 0 = not yet. +# '\n'(10) ends a line without entering the buffer and drops a preceding '\r'(13); a full buffer +# (GUARD_LINE_CAP-1) also ends a line. A NUL is written after each append; Guard_Fill is the length. def GuardFeedByte(Ch: int) -> int: global Guard_Fill - ub: BYTEPTR = BYTEPTR(Guard_Line) # 读取用字节视图(同 ParseOkOne/旧 ReadConfirmLine) + ub: BYTEPTR = BYTEPTR(Guard_Line) # byte view for reading if Ch == 10: # '\n' if Guard_Fill > 0 and int(ub[Guard_Fill - 1]) == 13: Guard_Fill -= 1 @@ -782,17 +644,17 @@ def GuardFeedByte(Ch: int) -> int: return 0 -# ---- 快修:未消费输入 FIFO 的三个小函数(两路取数共用;见模块头「快修:批量输入与可疑行」)---- +# ---- unconsumed-input FIFO helpers (shared; see module head "Quick-fix") ---- -# 清空 FIFO(新一轮确认开始时调用:新窗口不带上一轮的残留)。 +# Clear the FIFO (called at the start of a new round: no residue from the previous round). def GuardPendReset(): global Guard_PendLen, Guard_PendPos Guard_PendLen = 0 Guard_PendPos = 0 -# 把 FIFO 里尚未消费的字节按顺序逐个喂给行缓冲。返回 1=途中凑满了一行(剩余字节留在 FIFO, -# 由下一次调用继续喂),0=FIFO 已耗尽且未成行(此时两个游标归零,FIFO 回到空且可从头部复用)。 +# Feed unconsumed FIFO bytes one by one into the line buffer. Returns 1 = a line completed mid-way +# (rest stays in the FIFO) or 0 = FIFO drained without a line (cursors reset, FIFO reused from head). def GuardFeedPending() -> int: global Guard_PendLen, Guard_PendPos ub: BYTEPTR = BYTEPTR(Guard_Pend) @@ -806,10 +668,8 @@ def GuardFeedPending() -> int: return 0 -# 把一个字节写入 FIFO(写入端游标 Guard_PendLen)。带边界守卫:满则丢弃该字节并记一行 stderr -# 告警,绝不越界写。按构造成立的不变量(见 GUARD_PEND_CAP 的推导),入队时 FIFO 必为空且 -# 单批入队量 <= 64 < GUARD_PEND_CAP,故"满"不可达;守卫只为让将来改大批上限时退化为"丢字节" -# 而不是写穿缓冲。 +# Write one byte into the FIFO (write cursor Guard_PendLen). A full-FIFO guard drops the byte with a +# stderr warning instead of overflowing; by the GUARD_PEND_CAP invariant "full" is unreachable. def GuardPendPut(Ch: int): global Guard_PendLen if Guard_PendLen >= GUARD_PEND_CAP: @@ -820,9 +680,8 @@ def GuardPendPut(Ch: int): Guard_PendLen += 1 -# 判定 stdin 形态:控制台 / 管道 / 其它流。 -# 依据:GetFileType 返回 FILE_TYPE_CHAR 的除控制台外还有 LPT 等字符设备, -# 故再用 GetConsoleMode 确认(成功 = 该句柄是控制台输入缓冲 ⇒ 可按记录读)。 +# Classify stdin: console / pipe / other stream. FILE_TYPE_CHAR also covers LPT etc., so +# GetConsoleMode is used to confirm a real console input buffer (=> read by records). def GuardInputKind(hIn: wb.HANDLE) -> int: T: ULONG = wf.GetFileType(hIn) if T == ULONG(FILE_TYPE_CHAR): @@ -832,28 +691,24 @@ def GuardInputKind(hIn: wb.HANDLE) -> int: return GUARD_IN_STREAM if T == ULONG(FILE_TYPE_PIPE): return GUARD_IN_PIPE - # FILE_TYPE_DISK(`< file` 重定向)/ FILE_TYPE_UNKNOWN(NUL 等)等一律按"其它流"处理 + # FILE_TYPE_DISK ('< file' redirect) / FILE_TYPE_UNKNOWN (NUL etc.) are all treated as "other stream" return GUARD_IN_STREAM # ============================================================ -# 加固 1:ReadConsoleInputExA 动态解析 +# Hardening 1: dynamic resolution of ReadConsoleInputExA # ============================================================ -# 解析 kernel32!ReadConsoleInputExA 到 Guard_ReadEx(整个进程只做一次)。 -# 返回 1=可用(含此前已解析成功),0=不可用(该导出不存在 ⇒ 调用方走降级路径)。 -# 解析不到的情形(旧系统无该导出)在这里被"记住"(GUARD_EX_MISSING),不再重复 -# GetProcAddress —— 取数路径每次判定都会调本函数,重复解析会白烧一次 API。 -# 两个字符串以字面量传(与 VDirs.vp:186 把 "USERPROFILE" 传给 LPCSTR 形参同一写法): -# * "kernel32.dll":该 DLL 在所有 Win32 进程里都已加载,GetModuleHandleA 只取句柄、不增引用; -# * "ReadConsoleInputExA":DLL 导出名带 A 后缀(无 Ex 这个名字),见模块头"加固 1"。 +# Resolve kernel32!ReadConsoleInputExA into Guard_ReadEx (once per process). Returns 1 = available +# (incl. previously resolved), 0 = unavailable (export absent => fallback). A failure is remembered +# (GUARD_EX_MISSING) so GetProcAddress is not retried on every pump call. def GuardResolveReadEx() -> int: global Guard_ReadEx, Guard_ReadExState if Guard_ReadExState == GUARD_EX_OK: return 1 if Guard_ReadExState == GUARD_EX_MISSING: return 0 - Guard_ReadExState = GUARD_EX_MISSING # 先按"不可用"记:任何一步失败都不再重试 + Guard_ReadExState = GUARD_EX_MISSING # record "unavailable" first: any failed step stops retries Hm: wb.HANDLE = wb.GetModuleHandleA("kernel32.dll") if Hm is None or Hm == wb.INVALID_HANDLE_VALUE: return 0 @@ -866,18 +721,15 @@ def GuardResolveReadEx() -> int: # ============================================================ -# 加固 2:控制事件处理例程(SetConsoleCtrlHandler) +# Hardening 2: control event handler (SetConsoleCtrlHandler) # ============================================================ -# 控制事件处理例程。职责极小:把进入"原始按键"模式前的控制台模式写回(幂等), -# 然后一律返回 FALSE(交还默认处理 ⇒ Ctrl+C/Ctrl+Break/关窗仍按原语义终止本进程)。 -# 取舍见模块头"加固 2"(返回 TRUE 必须自行 ExitProcess,否则会把"用户主动 Ctrl+C" -# 变成"看门狗继续跑到下一轮再杀父")。 -# 线程安全:本函数可能被系统在**新线程**上调用,只读三个模块级标量(Guard_RawActive / -# Guard_ConHandle / Guard_SavedMode)并调用一次 SetConsoleMode;不分配内存、不写缓冲、 -# 不碰 _mbuddy 池与 VLog 缓冲,故不与主线程的池/日志/扫描缓冲并发写。 -# 被 TerminateProcess 强杀(父进程 StopWatchdog 回收、心跳失败杀父)时不执行本函数。 -# 返回值写 0(= FALSE;本模块只 import 了 w32.win32base as wb,没有 FALSE 这个裸名) +# Control event handler: restore the pre-raw console mode (idempotent) then always return FALSE +# (default handling preserved, so Ctrl+C/Ctrl+Break/close still terminate the process). +# Thread-safe by construction: runs on a system thread, only reads three module scalars and calls +# SetConsoleMode once (no allocation, no pool / VLog / scan buffer access). +# Not called when force-killed by TerminateProcess (StopWatchdog reclaim / heartbeat-fail kill). +# Return 0 (= FALSE; this module imports w32.win32base as wb and has no bare FALSE name) def GuardCtrlHandler(CtrlType: ULONG) -> BOOL: if Guard_RawActive == 0: return 0 @@ -886,7 +738,7 @@ def GuardCtrlHandler(CtrlType: ULONG) -> BOOL: return 0 -# 摘除控制事件处理例程(幂等;未注册时一次 API 都不调用)。 +# Remove the control handler (idempotent; no API call when not registered). def GuardRemoveCtrlHandler(): global Guard_CtrlHandlerOn if Guard_CtrlHandlerOn == 0: @@ -895,14 +747,11 @@ def GuardRemoveCtrlHandler(): Guard_CtrlHandlerOn = 0 -# 进入"原始按键"控制台模式:清 ENABLE_LINE_INPUT 与 ENABLE_ECHO_INPUT(取舍见模块头)。 -# 返回 1=可用(已切换,或两位本就未置位而无需切换),0=GetConsoleMode/SetConsoleMode 失败 -# (此时模式未改动,调用方按失败处理)。 -# 加固 2 的注册顺序(都在"确实要改模式"的分支里,两位本就未置位时不注册): -# ① 置 Guard_RawActive=1 并登记控制台句柄(先让 handler 具备写回所需的一切); -# ② 注册 handler(best-effort:失败只告警,正常路径仍在 GuardLeaveRaw 写回模式); -# ③ 真正 SetConsoleMode。这样 Ctrl+C 落在②与③之间时 handler 写回的是"尚未改动的原模式" -# (幂等空操作),不会留下"已清行输入 + 无 handler"的窗口。 +# Enter raw-key console mode: clear ENABLE_LINE_INPUT and ENABLE_ECHO_INPUT (see module head). +# Returns 1 = usable (switched, or both bits were already clear), 0 = Get/SetConsoleMode failed. +# Registration order (Hardening 2): set Guard_RawActive=1 and record the handle, register the +# handler (best-effort), then SetConsoleMode, so a Ctrl+C between the last two steps restores the +# still-unchanged mode (idempotent no-op). def GuardEnterRaw(hIn: wb.HANDLE) -> int: global Guard_SavedMode, Guard_RawActive, Guard_ConHandle, Guard_CtrlHandlerOn Mode: ULONG = 0 @@ -912,7 +761,7 @@ def GuardEnterRaw(hIn: wb.HANDLE) -> int: Guard_RawActive = 0 New: ULONG = Mode & ULONG(GUARD_RAW_CLEAR_MASK) if New == Mode: - return 1 # 无需改模式 ⇒ 也无需 handler + return 1 # mode unchanged => no handler needed Guard_ConHandle = hIn Guard_RawActive = 1 if SetConsoleCtrlHandler(GuardCtrlHandler, GUARD_CTRL_ADD) != 0: @@ -920,8 +769,8 @@ def GuardEnterRaw(hIn: wb.HANDLE) -> int: else: VLog.LogStr("[confirm] 警告: SetConsoleCtrlHandler 注册失败,Ctrl+C 时该终端可能残留已清行输入的模式\n") if wc.SetConsoleMode(hIn, New) == 0: - # 先清 Guard_RawActive 再清 Guard_SavedMode:handler 只在 RawActive!=0 时写回, - # 这个顺序保证它绝不会把"清零后的 SavedMode(=0)"写进控制台(此刻模式其实没变)。 + # Clear Guard_RawActive before Guard_SavedMode: the handler writes back only when + # RawActive != 0, so it never pushes the zeroed SavedMode to the console. Guard_RawActive = 0 Guard_SavedMode = 0 GuardRemoveCtrlHandler() @@ -929,10 +778,9 @@ def GuardEnterRaw(hIn: wb.HANDLE) -> int: return 1 -# 写回进入前的控制台模式(幂等;未实际切换过则一次 API 都不调用)。 -# 顺序:写回模式 → 清 Guard_RawActive → 摘除 handler。这个顺序保证任何时刻都不存在 -# "已清行输入但没有 handler"的窗口(摘除后已无待恢复的状态);反序则会在两步之间留一个 -# 这样的窗口。注册与"已切换模式"同生命周期 ⇒ Guard_RawActive==0 ⇔ 未注册(无泄漏)。 +# Write back the pre-entry console mode (idempotent; no API call if never switched). Order: write +# back, clear Guard_RawActive, remove the handler; this order leaves no "line input cleared but no +# handler" window. Registration and RawActive share one lifetime (RawActive==0 <=> not registered). def GuardLeaveRaw(hIn: wb.HANDLE): global Guard_RawActive if Guard_RawActive == 0: @@ -942,31 +790,18 @@ def GuardLeaveRaw(hIn: wb.HANDLE): GuardRemoveCtrlHandler() -# 控制台路径:一次非阻塞取数。返回 1=已凑满一行,0=无待读记录/尚未成行,-1=读取失败。 -# 只处理 KEY_EVENT(0x0001) 且 bKeyDown 的字符:可打印 ASCII(32..126) 与制表符(9) 累积, -# Enter 的 '\r'(13) 归一成 '\n' 后入队;其余(按键抬起、方向键/功能键、退格、非 ASCII 输入) -# 一律忽略(本路径不做行编辑)。 -# 快修后的三步(与 GuardPumpStream 同构,未消费字节不再丢): -# ① 先 GuardFeedPending():FIFO 里可能已有上一批留下的完整行 ⇒ 凑满即 return 1(不发起读取); -# ② 需要新数据时才取一批记录(本轮仍是"一次调用最多 GUARD_REC_MAX 条"); -# ③ 过滤后的字符**整批**压入 FIFO(GuardPendPut),再 GuardFeedPending() 并返回其结果—— -# 该批可能自成一行,也可能只成一行的一部分,剩余字节留在 FIFO 由下次调用继续喂。 -# 取数分两路(决定走哪一路的只有"ReadConsoleInputExA 是否解析得到"): -# * 主路径(加固 1):ReadConsoleInputExA + CONSOLE_READ_NOWAIT —— 文档明写"输入缓冲 -# 为空也立即返回",故这里**没有**"先查数量再读"的两步组合,读取不再可能阻塞; -# 返回 0 条记录即"本轮暂无数据"。 -# * 降级路径(解析不到该导出):有界等"可读"(上限 = min(剩余毫秒, GUARD_LEGACY_WAIT_MS)) -# → GetNumberOfConsoleInputEvents → ReadConsoleInputA。残余竞态:查询到 N>0 之后若 -# 另一读者取走记录,ReadConsoleInputA 会等**下一条记录**(该函数无超时参数)—— -# 窗口极小但非零,见模块头"加固 1"。此处能做的收紧是:①这一步之前的等待不占满剩余 -# 预算;②剩余为 0 时直接返回、不发起这次读(绝不在"已过期"的 deadline 上进入阻塞读)。 +# Console path: one non-blocking pump. Returns 1 = line complete, 0 = no pending record / not yet, +# -1 = read error. Accepts only KEY_EVENT(0x0001) with bKeyDown: printable ASCII(32..126) and tab(9), +# Enter '\r'(13) normalized to '\n'; all else ignored. Steps: feed the FIFO, else read one batch +# (primary: ReadConsoleInputExA+NOWAIT; fallback: bounded wait -> query -> ReadConsoleInputA), push +# filtered chars, then feed again. def GuardPumpConsole(hIn: wb.HANDLE, Deadline: ULONGLONG) -> int: - if GuardFeedPending() != 0: # ① FIFO 里已有整行 ⇒ 直接交回,不发起读取 + if GuardFeedPending() != 0: # (1) FIFO already has a full line => return it, no read return 1 Read: ULONG = 0 if GuardResolveReadEx() != 0: - # Guard_ReadEx 为模块级 t.Callable,其返回类型签名已随模块级 AnnAssign 登记 - # (TranslatorGenMixin.handle_module_level_var),故可直接间接调用。 + # Guard_ReadEx is a module-level t.Callable whose return signature was registered with the + # module-level annotation, so it can be called indirectly. if Guard_ReadEx(hIn, t.CPtr(Guard_Recs), ULONG(GUARD_REC_MAX), &Read, USHORT(CONSOLE_READ_NOWAIT)) == 0: return -1 @@ -999,30 +834,22 @@ def GuardPumpConsole(hIn: wb.HANDLE, Deadline: ULONGLONG) -> int: continue Ch: int = int(VPe.ReadU16(ub, Base + GUARD_KEY_CHAR_OFF)) if Ch == 13: - Ch = 10 # Enter → 行结束(与 '\n' 同一口径) + Ch = 10 # Enter -> end of line (same as '\n') elif Ch != 9 and (Ch < 32 or Ch > 126): - continue # 非可打印/非 ASCII:忽略 - GuardPendPut(Ch) # ③ 过滤后压入 FIFO(本次调用至多 16 字节) - if GuardFeedPending() != 0: # ③ 再喂 FIFO;剩余字节留给下次调用 + continue # non-printable / non-ASCII: ignore + GuardPendPut(Ch) # (3) push filtered chars into the FIFO (at most 16 bytes per call) + if GuardFeedPending() != 0: # (3) feed the FIFO again; remaining bytes stay for the next call return 1 return 0 -# 管道/重定向路径:一次非阻塞取数。返回 1=已凑满一行,0=当前无可读字节(尚未成行), -# -1=读取失败,GUARD_PUMP_EOF=不可能再出现输入(ReadFile 读到 0 字节)。 -# 管道路径先 PeekNamedPipe 问"当前真正可用的字节数",只读 min(可用, 块上限); -# 非管道(磁盘文件重定向等)无法预询可读量,改读一块有界字节(<= GUARD_CHUNK_CAP), -# 读到 0 字节即 EOF。ReadFile 只在"已知有数据(管道路径)"或"有界块(其它路径)"时调用。 -# 快修后的三步(同 GuardPumpConsole):① 先 GuardFeedPending()(FIFO 里可能已有整行); -# ② 需要新数据才读一批(读入量同时受 GUARD_CHUNK_CAP 与 GUARD_PEND_CAP 约束,保证整批放得进 -# FIFO);③ 把该批**全部**字节压入 FIFO(不再"凑满一行就丢弃同批剩余字节"),再 GuardFeedPending() -# 并返回其结果——同批的剩余字节留在 FIFO,由下一次调用继续喂。 -# 取舍:PeekNamedPipe 报"可用 0 字节"时统一按"暂无数据"推进(不据此判 EOF)—— -# 该函数无法区分"写端仍开、暂无数据"与"写端已关、无数据",据此判 EOF 有误杀风险; -# 由调用方的 deadline 兜底(到点按超时失败收尾,与旧实现"EOF 当空行失败"的最终结果一致, -# 仅失败时机由"读到 EOF"变为"到点")。EOF 仅在非管道流上可判(读盘重定向读完即 0 字节)。 +# Pipe/redirect path: one non-blocking pump. Returns 1 = line complete, 0 = no readable bytes, +# -1 = read error, GUARD_PUMP_EOF = no more input (0-byte ReadFile). Pipes peek the available count +# and read only that; other streams read one bounded block (<= GUARD_CHUNK_CAP). Steps: feed FIFO, +# read a batch, push the whole batch, feed again. A peek of 0 means "no data", not EOF (the deadline +# decides the timeout). def GuardPumpStream(hIn: wb.HANDLE, Kind: int) -> int: - if GuardFeedPending() != 0: # ① FIFO 里已有整行 ⇒ 直接交回,不发起读取 + if GuardFeedPending() != 0: # (1) FIFO already has a full line => return it, no read return 1 N: ULONG = ULONG(GUARD_CHUNK_CAP) if Kind == GUARD_IN_PIPE: @@ -1033,10 +860,8 @@ def GuardPumpStream(hIn: wb.HANDLE, Kind: int) -> int: return 0 if Avail < N: N = Avail - # ② 读入量的两个上界各夹一次(顺序无关,两者都必须成立): - # * 必须 ≤ 读块缓冲的实际大小 GUARD_CHUNK_CAP —— Guard_Chunk 正是按该值分配的, - # 给 ReadFile 传更大的 N 会写穿缓冲(当前 N 初值即 GUARD_CHUNK_CAP,本夹是防将来改动); - # * 必须 ≤ GUARD_PEND_CAP —— 本批要整批放进 FIFO,故声明处要求 GUARD_PEND_CAP >= GUARD_CHUNK_CAP。 + # (2) Clamp the read size by both bounds (order-independent): <= GUARD_CHUNK_CAP (the actual chunk + # buffer size; a larger N would overflow it) and <= GUARD_PEND_CAP (the whole batch must fit the FIFO). if N > ULONG(GUARD_CHUNK_CAP): N = ULONG(GUARD_CHUNK_CAP) if N > ULONG(GUARD_PEND_CAP): @@ -1048,19 +873,18 @@ def GuardPumpStream(hIn: wb.HANDLE, Kind: int) -> int: return GUARD_PUMP_EOF ub: BYTEPTR = BYTEPTR(Guard_Chunk) for i in range(int(Br)): - GuardPendPut(int(ub[i])) # ③ 先整批入队(Br <= N <= GUARD_PEND_CAP) - if GuardFeedPending() != 0: # ③ 再喂 FIFO;剩余字节留给下次调用 + GuardPendPut(int(ub[i])) # (3) enqueue the whole batch first (Br <= N <= GUARD_PEND_CAP) + if GuardFeedPending() != 0: # (3) feed the FIFO again; remaining bytes stay for the next call return 1 return 0 # ============================================================ -# 看门狗侧:失败处理 + 主循环 +# watchdog side: failure handling + main loop # ============================================================ -# 失败路径:stderr 打明原因 → stdout 逐字输出唯一一行 {"exit":1, "msg": "timeout/ok!=1"}\n -# → TerminateProcess(父进程) → sys.exit(1)。 -# 父进程被强杀(TerminateProcess 不做任何收尾):不写缓存、不再打本轮数组 JSON。 +# Failure path: reason to stderr -> the single stdout line {"exit":1, "msg": "timeout/ok!=1"}\n -> +# TerminateProcess(parent) -> sys.exit(1). The parent is killed with no cleanup (no cache, no JSON). def GuardWatchFail(hParent: wb.HANDLE, Reason: str): if Reason is not None: VLog.LogFmtS1("[confirm] 失败: %s\n", Reason) @@ -1071,7 +895,7 @@ def GuardWatchFail(hParent: wb.HANDLE, Reason: str): sys.exit(1) -# 看门狗侧缓冲一次性分配;返回 1=可用,0=失败 +# Allocate watchdog-side buffers once; returns 1 = usable, 0 = failure def GuardWatchInit() -> int: global Guard_Line, Guard_Stamp, Guard_Dl, Guard_Recs, Guard_Chunk, Guard_Pend if Guard_Line is None: @@ -1088,77 +912,55 @@ def GuardWatchInit() -> int: return 1 -# 从提示发出起算,在 TimeoutMs 内等"完整的一行且分类通过"(控制台/管道/重定向两路共用)。 -# 返回 1=确认成功(Guard_Line/Guard_Fill 为本轮输入);0=父进程已退出(调用方自退)。 -# 失败(期限到点仍未收到合法 {"ok": 1} / 形状合法但值不对 / 读失败 / EOF)在本函数内收尾: -# 先恢复控制台模式,再 GuardWatchFail(打 stdout 失败 JSON + 杀父 + 退出码 1),故失败路径不返回。 -# -# 全程非阻塞、由 deadline 驱动(快修后:**先取数、再等待/节拍**): -# ① 进入时清空未消费输入 FIFO 与行缓冲(GuardPendReset):新窗口不带上一轮的残留; -# ② 循环每轮先取数(GuardPump* 内部先喂 FIFO)——FIFO 里可能已有上一批留下的整行;若反过来 -# 先 WaitForMultipleObjects,会因"stdin 此刻无可读数据、句柄不置信号"白等到期限耗尽; -# ③ 取到整行后先分类(GuardClassifyLine;check 行由 ParseCheckLine 优先判并转发); -# ④ 取数无果时才等:WaitForMultipleObjects({stdin, 父进程句柄}, min(剩余, GUARD_POLL_MS))—— -# 上限取 min(剩余, 20ms) 而非整个剩余预算:读端已关的管道会永久置信号而可读字节为 0 -# (PeekNamedPipe 无法区分"写端仍在/已关"),"全剩余"等待会立即返回并形成忙等;短上限把节拍 -# 固定为 20ms(等价于旧实现取数后的 Sleep(20ms)),且父进程退出在 ≤20ms 内仍然可见; -# ⑤ 取数本身不阻塞(控制台:ReadConsoleInputExA + CONSOLE_READ_NOWAIT 立即返回;管道:可用字节 -# > 0 才读;其它流:有界块),不存在等下一次按键/下一次回车才返回的读取;控制台降级路径 -# (老系统无该导出)残余竞态见模块头"加固 1"; -# ⑥ 分类结果三态:合法 {"ok": 1} ⇒ 成功返回;形状合法但值不对 ⇒ 致命失败(口径不放宽); -# 形状可疑 ⇒ stderr 回显(截断)后忽略该行并继续等,**期限(deadline)不变**; -# ⑦ 被忽略的行(可疑行 / 转发出去的 check 行)**不重置期限**,也不会绕过期限:两条忽略路径 -# 都落到③④之间的"剩余为 0 ⇒ 超时失败"检查上 ⇒ 持续涌入的这类行同样会在期限到点收尾 -# ("期限到点仍未收到合法 {"ok": 1} 才判失败"对"无输入"与"只有被忽略的行"一致成立)。 +# Wait from the prompt on, within TimeoutMs, for a complete classified line (all paths). Returns +# 1 = confirmed (Guard_Line/Guard_Fill hold the input), 0 = parent exited. Failures are finalized +# here (restore mode, then GuardWatchFail: failure JSON + kill parent + exit 1). Non-blocking and +# deadline-driven: clear FIFO/line buffer, pump first (feeding the FIFO), classify, wait only when +# nothing was read; ignored lines never reset the deadline. def GuardWaitConfirm(hIn: wb.HANDLE, hParent: wb.HANDLE, Kind: int, TimeoutMs: int) -> int: global Guard_Fill - GuardPendReset() # ① 新一轮:FIFO 与行缓冲都不带上一轮残留 + GuardPendReset() # (1) new round: FIFO and line buffer carry no residue Guard_Fill = 0 Guard_Line[0] = 0 if Kind == GUARD_IN_CONSOLE: - # 清 ENABLE_LINE_INPUT:清掉后回车不再由系统"行输入"消化,而以 KEY_EVENT 到达, - # 我们才能自己判"一整行"(依据见模块头"控制台模式取舍")。 - # 拿不到/改不了控制台模式 ⇒ 无法保证"timeout 内可判定"(旧行为是 ReadFile 等到回车), - # 故按既有约定失败收尾,不静默降级为会挂死的阻塞等待。 + # Clear ENABLE_LINE_INPUT so Enter arrives as a KEY_EVENT we can judge ourselves (see module + # head "Console mode tradeoff"). If the mode cannot be read/switched, fail as usual. if GuardEnterRaw(hIn) == 0: GuardWatchFail(hParent, "控制台模式读取/切换失败") return 0 Deadline: ULONGLONG = wb.GetTickCount64() + ULONGLONG(uint32(TimeoutMs)) while True: - # ② 先取数(GuardPump* 内部先喂 FIFO):FIFO 里已有整行时不会白等一次 20ms + # (2) pump first (GuardPump* feeds the FIFO first): no wasted 20ms wait when a line is buffered R: int = 0 if Kind == GUARD_IN_CONSOLE: R = GuardPumpConsole(hIn, Deadline) else: R = GuardPumpStream(hIn, Kind) if R == 1: - # 一行凑满 ⇒ 先分类(用仓库 json 库解析,不做字符串猜测): - # * check 请求(对象含 "check" 字符串 与 "id" 整数)⇒ 原样转发到 IPC 写端; - # 该行**不计入心跳、也不判失败**:期限继续走,用户仍需在期限内发 {"ok": 1}。 - # * 其余 ⇒ 交 GuardClassifyLine 三态判定(ParseOkOne 口径不变)。 + # A completed line is classified first (via the repo json library): a check request is + # forwarded verbatim and does not count as a heartbeat; anything else goes to + # GuardClassifyLine (ParseOkOne unchanged). if VCheck.ParseCheckLine(str(Guard_Line)) != 0: GuardForwardLine() else: Cls: int = GuardClassifyLine(Guard_Line, Guard_Fill) if Cls == GUARD_LINE_OK: - break # 合法 {"ok": 1} ⇒ 出循环后收尾成功 + break # valid {"ok": 1} => break; success finalized after the loop if Cls == GUARD_LINE_BADOK: - # 形状合法但值不对("用户真的输错")⇒ 与旧实现同一失败口径与同一 stderr 文案 + # valid shape but bad value => fatal failure with the same stderr text GuardLeaveRaw(hIn) VLog.LogFmtS1("[confirm] 收到非 {\"ok\": 1} 的输入: %s\n", str(Guard_Line)) GuardWatchFail(hParent, "ok != 1") return 0 - # 形状可疑(首字符非 '{' / 不含 '}' / 长度异常,含空行)⇒ 忽略该行并回显(截断) + # suspect shape (first char not '{' / no '}' / bad length, incl. empty) => echo (truncated) and ignore if Guard_Fill > GUARD_SUSPECT_ECHO_MAX: - Guard_Line[GUARD_SUSPECT_ECHO_MAX] = 0 # 只回显前缀(该行随后整个丢弃) + Guard_Line[GUARD_SUSPECT_ECHO_MAX] = 0 # echo only the prefix (the line is then dropped) VLog.LogFmtS1("[confirm] 忽略可疑输入行: %s\n", str(Guard_Line)) Guard_Fill = 0 Guard_Line[0] = 0 - # 两条"忽略"路径(check 转发 / 可疑行)都**落到下方的期限检查 + 等待**: - # 忽略不重置期限(Deadline 在循环外算好),但期限一旦到点仍按超时失败收尾 - # ⇒ 持续涌入的可疑行/check 行不会让本窗口无限延长("期限到点仍未收到合法 - # {"ok": 1} 才判失败"这一条对"无输入"与"只有被忽略的行"同样成立)。 - # 此处 R==1,故下方 "if R < 0" 分支不会命中,控制流直接进入等待段。 + # Both ignore paths fall through to the deadline check + wait below. Ignoring does not + # reset the deadline, so a stream of suspect/check lines still ends at the deadline + # (R==1 here, so the "R<0" branch is skipped and control goes straight to the wait). if R < 0: GuardLeaveRaw(hIn) if R == GUARD_PUMP_EOF: @@ -1166,7 +968,7 @@ def GuardWaitConfirm(hIn: wb.HANDLE, hParent: wb.HANDLE, Kind: int, TimeoutMs: i else: GuardWatchFail(hParent, "读取 stdin 失败") return 0 - # ③ 本轮取数无果 ⇒ 等"stdin 可读 / 父进程退出 / 到点",上限 = min(剩余, GUARD_POLL_MS) + # (3) nothing read: wait for stdin readable / parent exit / deadline, cap min(remaining, GUARD_POLL_MS) Rem2: ULONG = GuardRemainMs(Deadline) if Rem2 == 0: GuardLeaveRaw(hIn) @@ -1180,25 +982,25 @@ def GuardWaitConfirm(hIn: wb.HANDLE, hParent: wb.HANDLE, Kind: int, TimeoutMs: i Hs.H1 = hParent Wm: ULONG = wsync.WaitForMultipleObjects(ULONG(WATCH_HANDLE_COUNT), &Hs, 0, Sl) - if Wm == ULONG(wb.WAIT_OBJECT_0) + ULONG(1): # 父进程句柄(索引 1)置信号 + if Wm == ULONG(wb.WAIT_OBJECT_0) + ULONG(1): # parent handle (index 1) signaled GuardLeaveRaw(hIn) return 0 if Wm == ULONG(wb.WAIT_FAILED): GuardLeaveRaw(hIn) GuardWatchFail(hParent, "等待输入失败") return 0 - # 其余取值(stdin 置信号 / WAIT_TIMEOUT)⇒ 回到循环顶部取数 + # other results (stdin signaled / WAIT_TIMEOUT) => loop back and pump again GuardLeaveRaw(hIn) return 1 -# confirm-watch 入口(App/main.vp 分派)。返回进程退出码。 -# 只做心跳确认,不碰扫盘/目录表/缓存。 +# confirm-watch entry (dispatched by App/main.vp). Returns the process exit code. +# Only does heartbeat confirmation; never touches the scan / directory table / cache. def RunConfirmWatch() -> int: global Guard_IpcWrite if VCli.ParseWatchArgs() != 0: return 1 - # 详细度由父进程经 --verbose 透传(父为 --verbose 时 SpawnWatchdog 会追加该开关) + # Verbosity is forwarded by the parent via --verbose (SpawnWatchdog appends it) VLog.SetVerbose(VCli.WatchVerbose()) IntervalSec: int = VCli.WatchInterval() TimeoutMs: int = VCli.WatchTimeout() @@ -1211,10 +1013,10 @@ def RunConfirmWatch() -> int: VLog.LogFmt3("[confirm] 看门狗启动: 每 %u 秒要求一次 {\"ok\": 1},超时 %u ms,父进程 pid=%u\n", sizet(IntervalSec), sizet(TimeoutMs), sizet(ParentPid)) - # 父进程句柄:同时用于"父退出检测(SYNCHRONIZE)"与"失败时终止父进程(PROCESS_TERMINATE)" + # Parent handle: used both for parent-exit detection (SYNCHRONIZE) and to terminate the parent hParent: wb.HANDLE = wp.OpenProcess(ULONG(WATCH_PARENT_ACCESS), 0, ULONG(ParentPid)) if hParent is None or hParent == wb.INVALID_HANDLE_VALUE: - # 拿不到父进程句柄 ⇒ 既无法监控也无法终止,无法履行契约:只记 stderr 并以 1 退出 + # No parent handle => cannot monitor or terminate; log to stderr and exit 1 VLog.LogFmt1("[confirm] 看门狗: OpenProcess 父进程失败 (GetLastError=%u),自身退出\n", sizet(wb.GetLastError())) return 1 @@ -1224,8 +1026,8 @@ def RunConfirmWatch() -> int: GuardWatchFail(hParent, "stdin 句柄无效") return 1 - # check 通道写端:父进程经 `--ipc-write <数值>` 传入的**句柄数值**,这里还原成 HANDLE - # (数值按 u64 位模式传递与解析;0/缺参 ⇒ 无通道,收到 check 行时只记 stderr)。 + # check channel write end: the handle value passed by the parent via `--ipc-write `, + # restored here (u64 bit pattern; 0 / missing => no channel, check lines only get a stderr note). if VCli.WatchIpcWrite() != 0: Guard_IpcWrite = t.CPtr(VCli.WatchIpcWrite()) else: @@ -1235,20 +1037,19 @@ def RunConfirmWatch() -> int: Ch0 = 1 VLog.VerboseFmt1("[confirm] 看门狗: check 通道=%u (1=已配置 0=无)\n", sizet(Ch0)) - # 输入形态只判一次(句柄在进程生命周期内不变);控制台与管道/重定向的取数路径不同。 + # Input kind is classified once (the handle never changes); console and pipe/redirect pump differently. Kind: int = GuardInputKind(hIn) VLog.VerboseFmt1("[confirm] 看门狗: stdin 形态=%u (1=控制台 2=管道 3=其它流)\n", sizet(Kind)) - # 控制台取数路径(加固 1):主路径=ReadConsoleInputExA+CONSOLE_READ_NOWAIT, - # 降级=GetNumberOfConsoleInputEvents+ReadConsoleInputA(该导出不存在的老系统)。 - # 这里提前解析一次(后续判定复用缓存的状态),顺带把实际走哪一路写进 --verbose 日志。 + # Console pump path (Hardening 1): primary ReadConsoleInputExA+NOWAIT, fallback query+ReadConsoleInputA. + # Resolve once here (later rounds reuse the cached state) and log the chosen path under --verbose. if Kind == GUARD_IN_CONSOLE: GuardResolveReadEx() VLog.VerboseFmt1("[confirm] 看门狗: 控制台取数路径=%u (1=ReadConsoleInputExA+NOWAIT 2=降级查询+ReadConsoleInputA)\n", sizet(Guard_ReadExState)) while True: - # 阶段 1:等 interval;父进程先退出则立即自退(无需父侧配合,杜绝孤儿) + # Phase 1: wait interval; if the parent exited first, exit immediately (no orphans) Ws: ULONG = wsync.WaitForSingleObject(hParent, IntervalMs) if Ws == wb.WAIT_OBJECT_0: VLog.LogStr("[confirm] 看门狗: 父进程已退出,自身退出\n") @@ -1259,15 +1060,15 @@ def RunConfirmWatch() -> int: GuardWatchFail(hParent, "等待父进程失败") return 1 - # 阶段 2:stderr 提示(期限自本行起算)+ 在 timeout 内非阻塞等"完整一行且分类通过" + # Phase 2: stderr prompt (deadline starts here) + non-blocking wait for a complete classified line VLog.LogFmt2("[confirm] 请在 %u ms 内输入 {\"ok\": 1}(每 %u 秒一次)\n", sizet(TimeoutMs), sizet(IntervalSec)) VLog.TimeStampNow(Guard_Stamp, GUARD_STAMP_CAP) VLog.TimeStampAfter(Guard_Dl, GUARD_STAMP_CAP, sizet(TimeoutMs)) VLog.LogFmtS2("[confirm] 当前 %s, 本轮期限 %s\n", str(Guard_Stamp), str(Guard_Dl)) - # 失败路径(期限到点/形状合法但值不对/读失败/EOF)已在 GuardWaitConfirm 内收尾并退出进程; - # 返回 0 只表示"父进程已退出",此时看门狗自退(不杀父、不打失败 JSON)。 + # Failure paths (deadline / bad value / read error / EOF) already finalized the process inside + # GuardWaitConfirm; a 0 return only means "parent exited", so the watchdog exits itself. if GuardWaitConfirm(hIn, hParent, Kind, TimeoutMs) == 0: VLog.LogStr("[confirm] 看门狗: 父进程已退出,自身退出\n") wb.CloseHandle(hParent) diff --git a/App/lib/VGuid.vp b/App/lib/VGuid.vp index 2dcd41f..d2afeef 100644 --- a/App/lib/VGuid.vp +++ b/App/lib/VGuid.vp @@ -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: diff --git a/App/lib/VJson.vp b/App/lib/VJson.vp index 872d203..636b6ff 100644 --- a/App/lib/VJson.vp +++ b/App/lib/VJson.vp @@ -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: diff --git a/App/lib/VLog.vp b/App/lib/VLog.vp index b688420..dc2720f 100644 --- a/App/lib/VLog.vp +++ b/App/lib/VLog.vp @@ -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: diff --git a/App/lib/VPe.vp b/App/lib/VPe.vp index 1cbfe02..6fb77d0 100644 --- a/App/lib/VPe.vp +++ b/App/lib/VPe.vp @@ -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) diff --git a/App/lib/VProc.vp b/App/lib/VProc.vp index d0cd4d6..7048264 100644 --- a/App/lib/VProc.vp +++ b/App/lib/VProc.vp @@ -11,61 +11,44 @@ import lib.VLog as VLog # ============================================================ -# VProc - 带超时的子进程执行( --version) +# VProc - subprocess execution with timeout ( --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`)。 -# 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: # ============================================================ -# 执行 " --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 " --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) - # --- 命令缓冲:"" --version --- + # --- command buffer: "" --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]:执行 --version,校验捕获输出前 5 字符是否为 "Viper" -# 返回 0=通过,1=失败(启动失败/超时/前缀不符) -# 行为增强:改用带超时探测(原实现无超时),其余日志与判定口径不变。 +# ver [2/3]: run --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 的字段声明一致。 # ============================================================ diff --git a/App/lib/VScan.vp b/App/lib/VScan.vp index db92b1f..dac18ee 100644 --- a/App/lib/VScan.vp +++ b/App/lib/VScan.vp @@ -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 ,见 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 ;文件侧粒度(目录侧仍是 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 ; 0 = no Sleep at either throttle point +Pace_File_Units: int = PACE_FILE_UNITS # --pace-units ; 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 可解析 + ` --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: diff --git a/App/lib/__init__.vp b/App/lib/__init__.vp index 22e73e4..d90b82c 100644 --- a/App/lib/__init__.vp +++ b/App/lib/__init__.vp @@ -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 diff --git a/App/main.vp b/App/main.vp index a0e83dd..f00798b 100644 --- a/App/main.vp +++ b/App/main.vp @@ -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)