i = 0 + while i < N: + 尾自增 -> or i in range(N):(8 文件 / 20 处;含 <= -> range(EXPR + 1)、带注解初始化、尾 i = i + 1)。余 19 处经守卫判定不可安全改写(复合条件 6、尾部非自增 7、体内重绑定/continue 2、循环后仍被读取 3、无整数初始化 1)。
1276 lines
80 KiB
Plaintext
1276 lines
80 KiB
Plaintext
import t, c
|
||
from stdint import *
|
||
import viperlib
|
||
import sys
|
||
import memhub
|
||
import lib.VLog as VLog
|
||
import lib.VCli as VCli
|
||
import lib.VCheck as VCheck
|
||
import lib.VPe as VPe
|
||
import w32.win32file as wf
|
||
import w32.win32base as wb
|
||
import w32.win32sync as wsync
|
||
import w32.win32process as wp
|
||
import w32.win32console as wc
|
||
|
||
|
||
# ============================================================
|
||
# VGuard - 心跳确认看门狗(--need-confirm <秒>;仅 scan 模式)
|
||
#
|
||
# 设计(本轮重构:与扫盘彻底解耦):
|
||
# * 父进程(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"约定不变。
|
||
#
|
||
# 本轮改动:确认输入由"阻塞式读一行"改为"非阻塞取数 + 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)为上限,到点即判超时;
|
||
# 读取只发生在"已确认有数据"之后,不存在"等到下一次按键 / 下一次回车才算完"的无界阻塞点。
|
||
#
|
||
# 控制台模式取舍(为什么清 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 回收、心跳失败杀父)不执行
|
||
# 任何用户态代码,仍无法恢复(该控制台输入模式可能残留"已清行输入")。
|
||
#
|
||
# 加固 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。
|
||
#
|
||
# 加固 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 缓冲 / 其它模块级缓冲(无并发写,见"静态论证")。
|
||
#
|
||
# 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 同步执行并回执)。
|
||
#
|
||
# 快修:批量输入与可疑行(两处缺陷的修复;语义变化见本节末尾)
|
||
# * 缺陷 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 输入为空行"变为"超时无输入",失败时机也
|
||
# 从"收到空行"推迟到"期限到点"。这是本轮**可接受的语义变化**。
|
||
#
|
||
# 为什么用子进程而不是线程(取舍):
|
||
# * 本仓有 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;可被"父进程句柄"精确回收(见下)。
|
||
#
|
||
# 生命周期与"无孤儿"保证:
|
||
# * 父正常结束(含 --once)/参数解析后提前返回:main.vp 调 StopWatchdog() →
|
||
# TerminateProcess(看门狗) + CloseHandle。
|
||
# * 父异常退出(Ctrl+C / 被 TerminateProcess):看门狗持有父进程句柄(OpenProcess),
|
||
# 父进程对象一终止即置信号 ⇒ WaitForSingleObject / WaitForMultipleObjects 立即返回,
|
||
# 看门狗自行退出(不需要父侧配合)。
|
||
# * 看门狗自身异常/崩溃:父进程的 Watch_Process 句柄仍可被 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)。
|
||
# ============================================================
|
||
|
||
define SYNCHRONIZE = 0x00100000 # winnt.h
|
||
# OpenProcess 需要的访问权限:PROCESS_TERMINATE(0x0001) | SYNCHRONIZE(0x00100000)
|
||
define WATCH_PARENT_ACCESS = 0x00100001
|
||
define CONFIRM_WATCH_CMD = "confirm-watch" # 内部隐藏子命令名
|
||
# stdin 行缓冲:既要放 {"ok": 1},也要放 check 请求行({"check":"<exe 绝对路径>","id":<uint64>},
|
||
# 路径可达 MAX_PATH 量级)⇒ 取 4096(与 VCheck.CK_LINE_MAX 同值)。行超此上限按"一行结束"
|
||
# 处理(与旧实现"缓冲写满即截断成一行"一致;超长的 check 行会因路径被截断而判 NOT_FOUND)。
|
||
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 WATCH_CMD_EXTRA_CAP = 256
|
||
define WATCH_HANDLE_COUNT = 2 # WaitForMultipleObjects 句柄个数
|
||
# 拉起后"存活校验"的等待窗口(毫秒)。代价:每次 scan 启动多等这么一会儿(README 已注明);
|
||
# 取 400 是"够看门狗跑完参数解析/OpenProcess/首次等待"与"不显著拖慢启动"之间的折中。
|
||
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)
|
||
define GUARD_RAW_CLEAR_MASK = 0xFFFFFFF9
|
||
# INPUT_RECORD 内 KEY_EVENT_RECORD 字段偏移(x64 布局推导见 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_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 字节)
|
||
|
||
# ---- 快修:未消费输入 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 只回显前缀,避免把超长输入整行打出去)
|
||
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 }" 之类的空白展开),超过即按"被撕裂的残行/噪声"处理。
|
||
define GUARD_OK_LINE_MAX = 256
|
||
|
||
# ---- check 请求通道(看门狗 → 父进程的 IPC 管道;依附 --need-confirm)----
|
||
# 行缓冲上限与看门狗侧 GUARD_LINE_CAP 同值(请求行由看门狗逐字节转发,长度不会超过它)
|
||
define IPC_LINE_CAP = 4096
|
||
define IPC_READ_CHUNK = 1024 # 单次 ReadFile 上限(只读"已确认可用"的部分)
|
||
define IPC_PIPE_SIZE = 65536 # CreatePipe 的 nSize 提示(默认过小,给足以免看门狗写阻塞)
|
||
|
||
# ---- 加固 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 节拍)。
|
||
define GUARD_LEGACY_WAIT_MS = 5
|
||
# SetConsoleCtrlHandler 的 Add 参数(consoleapi.h 的 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 = 当前可读字节数。
|
||
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 时摘除。
|
||
extern def SetConsoleCtrlHandler(HandlerRoutine: t.CPtr, Add: BOOL) -> BOOL
|
||
|
||
# 由 App/main.vp 注入(与其它模块同一 MemBuddy 实例)
|
||
_mbuddy: memhub.MemBuddy*
|
||
|
||
# 看门狗句柄(父进程侧;StopWatchdog 负责关闭)
|
||
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=当前处于已切换模式(退出前必须写回)
|
||
|
||
# 看门狗侧:check 通道的管道写端(由父进程经 `--ipc-write <数值>` 传入;None=无通道)。
|
||
# 只用于"原样转发 check 请求行",写失败仅记 stderr(不改变心跳语义)。
|
||
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)
|
||
|
||
# 加固 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。
|
||
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 负责摘除)
|
||
|
||
|
||
# WaitForMultipleObjects 需要一个"句柄数组"(HANDLE 即 VOIDPTR,x64 下 8 字节)。
|
||
# 用两个 HANDLE 字段的类型化结构体,按声明顺序连续存放 = 16 字节,恰为 Win32 期望。
|
||
class WATCH_HANDLES():
|
||
H0: wb.HANDLE
|
||
H1: wb.HANDLE
|
||
|
||
|
||
# ============================================================
|
||
# check 请求通道:IPC 管道(父进程侧:建立 / 关闭 / 非阻塞轮询)
|
||
#
|
||
# 为什么用"管道 + 数值句柄经命令行传递":
|
||
# * 父子之间已有 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` 子进程时不会把这个写端一起继承出去(读端本就不可继承)。
|
||
#
|
||
# 父进程侧读取**绝不阻塞**:
|
||
# * 先 PeekNamedPipe 取"当前真正可用的字节数";可用为 0 就直接返回(不调用 ReadFile);
|
||
# * 只读 min(可用, IPC_READ_CHUNK) 字节——ReadFile 在"确有数据可读"时立即返回;
|
||
# * 每次轮询的数据量上界 = 进入时管道中已有的字节数(排空后即返回),无任何"等下一次到来",
|
||
# 故轮询点可以放在扫盘的高频路径上(PaceDir/PaceFile)而不会拖慢/挂住扫盘。
|
||
#
|
||
# 行切分:与看门狗侧同一口径——'\n' 结束一行(看门狗把行原样转发时补的那个 '\n');
|
||
# 行内容不含 '\n'(看门狗侧 GuardFeedByte 已把 '\n' 排除在缓冲之外)。
|
||
# 超长行(≥ IPC_LINE_CAP-1 仍未见到 '\n')按"丢弃该行"处理(Ipc_Fill 归零),
|
||
# 既不越界也不把两行粘成一行。
|
||
# ============================================================
|
||
|
||
# 建立 IPC 管道:成功返回**写端句柄**(由调用方交给看门狗并在用完后 CloseHandle),
|
||
# 失败返回 None(调用方按"看门狗拉起失败=致命"处理)。读端存入 Ipc_Read。
|
||
def GuardIpcCreate() -> wb.HANDLE:
|
||
global Ipc_Read, Ipc_Line, Ipc_Chunk, Ipc_Fill
|
||
GuardIpcClose() # 幂等:如已有旧管道先关掉
|
||
if Ipc_Line is None:
|
||
Ipc_Line = _mbuddy.alloc(IPC_LINE_CAP)
|
||
if Ipc_Chunk is None:
|
||
Ipc_Chunk = _mbuddy.alloc(IPC_READ_CHUNK)
|
||
if Ipc_Line is None or Ipc_Chunk is None:
|
||
return None
|
||
Sa: wb.SECURITY_ATTRIBUTES = wb.SECURITY_ATTRIBUTES()
|
||
Sa.nLength = uint32(wb.SECURITY_ATTRIBUTES.__sizeof__())
|
||
Sa.lpSecurityDescriptor = t.CPtr(0)
|
||
Sa.bInheritHandle = 1 # 两端先都建为可继承,随后单方面清读端
|
||
hRead: wb.HANDLE = None
|
||
hWrite: wb.HANDLE = None
|
||
if wf.CreatePipe(&hRead, &hWrite,
|
||
&Sa, ULONG(IPC_PIPE_SIZE)) == 0:
|
||
VLog.LogFmt1("[confirm] 警告: check 通道 CreatePipe 失败 (GetLastError=%u)\n",
|
||
sizet(wb.GetLastError()))
|
||
return None
|
||
# 读端只在父进程用:清掉继承位,避免被任何子进程(看门狗、--version 探测)继承
|
||
wf.SetHandleInformation(hRead, wf.HANDLE_FLAG_INHERIT, 0)
|
||
wf.SetHandleInformation(hWrite, wf.HANDLE_FLAG_INHERIT, wf.HANDLE_FLAG_INHERIT)
|
||
Ipc_Read = hRead
|
||
Ipc_Fill = 0
|
||
return hWrite
|
||
|
||
|
||
# 关闭 IPC 读端(幂等;StopWatchdog 与 SpawnWatchdog 的失败路径都会调)。
|
||
def GuardIpcClose():
|
||
global Ipc_Read, Ipc_Fill
|
||
if Ipc_Read is not None:
|
||
wb.CloseHandle(Ipc_Read)
|
||
Ipc_Read = None
|
||
Ipc_Fill = 0
|
||
|
||
|
||
# 非阻塞轮询 IPC 读端:把"当前已可用"的字节全部取走并按 '\n' 切行,逐行交给 VCheck 处理
|
||
# (处理是同步的:check 的"读 .PRVERSN → --version → 回执"整段在本次调用内完成)。
|
||
# 返回本次已处理的 check 请求行数(0 = 无数据/非 check 行/通道未建立)。
|
||
def PollIpc() -> int:
|
||
global Ipc_Fill
|
||
if Ipc_Read is None or Ipc_Read == wb.INVALID_HANDLE_VALUE:
|
||
return 0
|
||
Done: int = 0
|
||
while True:
|
||
Avail: ULONG = 0
|
||
if PeekNamedPipe(Ipc_Read, t.CPtr(0), 0, t.CPtr(0), &Avail, t.CPtr(0)) == 0:
|
||
return Done # 管道异常:本轮不再读(不阻塞、不重试)
|
||
if Avail == 0:
|
||
return Done # 无数据 ⇒ 立即返回(轮询点的高频路径)
|
||
N: ULONG = ULONG(IPC_READ_CHUNK)
|
||
if Avail < N:
|
||
N = Avail # 只读"确已有数据"的部分 ⇒ ReadFile 不会阻塞
|
||
Br: ULONG = 0
|
||
if wf.ReadFile(Ipc_Read, Ipc_Chunk, N, &Br, None) == 0:
|
||
return Done
|
||
if Br == 0:
|
||
return Done
|
||
ub: BYTEPTR = BYTEPTR(Ipc_Chunk)
|
||
i: int = 0
|
||
while i < int(Br):
|
||
Ch: int = int(ub[i])
|
||
i += 1
|
||
if Ch == 10: # '\n':一行结束
|
||
Ipc_Line[Ipc_Fill] = 0
|
||
if Ipc_Fill > 0:
|
||
if VCheck.HandleCheckLine(str(Ipc_Line)) != 0:
|
||
Done += 1
|
||
Ipc_Fill = 0
|
||
else:
|
||
if Ipc_Fill >= IPC_LINE_CAP - 1:
|
||
Ipc_Fill = 0 # 超长行:丢弃该行(不越界、不与下一行粘连)
|
||
else:
|
||
Ipc_Line[Ipc_Fill] = char(Ch)
|
||
Ipc_Fill += 1
|
||
|
||
|
||
# ============================================================
|
||
# 父进程侧:拉起 / 停止看门狗
|
||
# ============================================================
|
||
|
||
# 取自身 exe 全路径(GetModuleFileNameA(NULL,...))。返回 1=成功(Out 已 NUL 结尾),0=失败。
|
||
def SelfExePath(Out: bytes, Cap: int) -> int:
|
||
if Out is None or Cap < 2:
|
||
return 0
|
||
n: ULONG = wf.GetModuleFileNameA(None, Out, ULONG(Cap))
|
||
if n == 0:
|
||
return 0
|
||
if sizet(n) >= sizet(Cap):
|
||
return 0 # 缓冲不足(路径被截断)⇒ 视为失败
|
||
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(致命)。
|
||
def SpawnWatchdog(IntervalSec: int, TimeoutMs: int) -> int:
|
||
global Watch_Process, Watch_Thread
|
||
if IntervalSec <= 0:
|
||
return 1 # 关闭:不拉起(调用方仅在 >0 时调用),视为"无需看门狗"
|
||
StopWatchdog() # 幂等:如已有旧看门狗先回收
|
||
|
||
Self: bytes = _mbuddy.alloc(SELF_PATH_CAP)
|
||
Cmd: bytes = _mbuddy.alloc(SELF_PATH_CAP + WATCH_CMD_EXTRA_CAP)
|
||
if Self is None or Cmd is None:
|
||
if Self is not None:
|
||
_mbuddy.free(t.CPtr(Self))
|
||
if Cmd is not None:
|
||
_mbuddy.free(t.CPtr(Cmd))
|
||
VLog.LogStr("[confirm] 警告: 看门狗缓冲分配失败,未启动\n")
|
||
return 0
|
||
if SelfExePath(Self, SELF_PATH_CAP) == 0:
|
||
_mbuddy.free(t.CPtr(Self))
|
||
_mbuddy.free(t.CPtr(Cmd))
|
||
VLog.LogStr("[confirm] 警告: GetModuleFileNameA 取自身路径失败,看门狗未启动\n")
|
||
return 0
|
||
|
||
# check 请求通道:先建管道(写端数值要写进命令行,必须在 CreateProcessA 之前拿到)。
|
||
# 失败即致命(与看门狗拉起失败同一处理):用户显式要求心跳确认,其附带的 check 通道
|
||
# 若静默缺失,等于悄悄少了一项被请求的能力。
|
||
IpcW: wb.HANDLE = GuardIpcCreate()
|
||
if IpcW is None:
|
||
_mbuddy.free(t.CPtr(Self))
|
||
_mbuddy.free(t.CPtr(Cmd))
|
||
VLog.LogStr("[confirm] 警告: check 通道管道建立失败,看门狗未启动\n")
|
||
return 0
|
||
|
||
# 命令行首 token 为自身全路径(引号包裹,兼容路径含空格);
|
||
# 父进程为 --verbose 时为子进程追加同名开关,使看门狗按同一详细度规则输出。
|
||
# --ipc-write <数值>:check 请求通道的管道**写端句柄数值**(父进程按 u64 位模式给出,
|
||
# 看门狗用 t.CPtr(数值) 还原句柄)。缺该参数(如手工调用)时看门狗侧按"无通道"处理。
|
||
Pid: ULONG = wp.GetCurrentProcessId()
|
||
Extra: str = ""
|
||
if VCli.Verbose() != 0:
|
||
Extra = " --verbose"
|
||
viperlib.snprintf(Cmd, sizet(SELF_PATH_CAP + WATCH_CMD_EXTRA_CAP),
|
||
"\"%s\" %s --interval %u --timeout %u --parent-pid %u --ipc-write %llu%s",
|
||
str(Self), CONFIRM_WATCH_CMD,
|
||
sizet(IntervalSec), sizet(TimeoutMs), sizet(Pid),
|
||
uint64(IpcW), Extra)
|
||
|
||
# 标准句柄:与父进程同一(继承控制台/重定向);stdin 也一并交给看门狗
|
||
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)
|
||
if hIn is None or hIn == wb.INVALID_HANDLE_VALUE:
|
||
wb.CloseHandle(IpcW)
|
||
GuardIpcClose()
|
||
_mbuddy.free(t.CPtr(Self))
|
||
_mbuddy.free(t.CPtr(Cmd))
|
||
VLog.LogStr("[confirm] 警告: stdin 句柄无效,看门狗未启动(心跳确认需要 stdin)\n")
|
||
return 0
|
||
# 尽力把标准句柄标记为可继承(控制台句柄通常本就随控制台继承;失败忽略)
|
||
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)
|
||
if hErr is not None and hErr != wb.INVALID_HANDLE_VALUE:
|
||
wf.SetHandleInformation(hErr, wf.HANDLE_FLAG_INHERIT, wf.HANDLE_FLAG_INHERIT)
|
||
|
||
Si: wp.STARTUPINFOA = wp.STARTUPINFOA()
|
||
Si.cb = uint32(wp.STARTUPINFOA.__sizeof__())
|
||
Si.dwFlags = wp.STARTF_USESTDHANDLES
|
||
Si.hStdInput = hIn
|
||
Si.hStdOutput = hOut
|
||
Si.hStdError = hErr
|
||
Pi: wp.PROCESS_INFORMATION = wp.PROCESS_INFORMATION()
|
||
|
||
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)
|
||
_mbuddy.free(t.CPtr(Self))
|
||
_mbuddy.free(t.CPtr(Cmd))
|
||
if Ok == 0:
|
||
wb.CloseHandle(IpcW)
|
||
GuardIpcClose()
|
||
VLog.LogFmt1("[confirm] 警告: 看门狗 CreateProcessA 失败 (GetLastError=%u),未启动\n",
|
||
sizet(CreateErr))
|
||
return 0
|
||
|
||
# 写端已由看门狗继承 ⇒ 父进程立即关掉自己的副本:这样后续任何 CreateProcessA
|
||
# (--version 探测)都不会把这个写端再继承出去。
|
||
wb.CloseHandle(IpcW)
|
||
|
||
Watch_Process = Pi.hProcess
|
||
Watch_Thread = Pi.hThread
|
||
|
||
# 存活校验(详见函数头注释):只把 WAIT_OBJECT_0(子进程已退出)判为致命。
|
||
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 造成双重关闭)。
|
||
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。
|
||
VLog.StdoutWrite("{\"exit\":1, \"msg\": \"timeout/ok!=1\"}\n")
|
||
return 0
|
||
VLog.LogFmt2("[confirm] 心跳看门狗已启动 (pid=%u, 每 %u 秒一次",
|
||
sizet(Pi.dwProcessId), sizet(IntervalSec))
|
||
VLog.LogFmt1(", 确认超时 %u ms)\n", sizet(TimeoutMs))
|
||
return 1
|
||
|
||
|
||
# 停止看门狗(幂等;父进程正常结束/提前返回时调用)。
|
||
# TerminateProcess 立即置"终止"标记,子进程不会再执行用户态代码;再 WaitForSingleObject
|
||
# 有界等待其真正退出,确保父进程结束前无遗留;最后关闭两个句柄与 check 通道的管道读端。
|
||
def StopWatchdog():
|
||
global Watch_Process, Watch_Thread
|
||
if Watch_Process is not None:
|
||
wp.TerminateProcess(Watch_Process, 0)
|
||
wsync.WaitForSingleObject(Watch_Process, 5000)
|
||
wb.CloseHandle(Watch_Process)
|
||
Watch_Process = None
|
||
if Watch_Thread is not None:
|
||
wb.CloseHandle(Watch_Thread)
|
||
Watch_Thread = None
|
||
GuardIpcClose()
|
||
|
||
|
||
# ============================================================
|
||
# 输入解析(沿用原判定口径)
|
||
# ============================================================
|
||
|
||
# 严格判定"存在 ok 且其值为 1"(容忍 key/值与冒号之间的空格 32 与制表 9)。
|
||
# 结构约束:`"ok"` 前一个非空白字符必须是 '{'(123) 或 ','(44)(或缓冲区起点)——
|
||
# 避免把某个字符串值里的 "ok" 误当成键;值必须是数字 1(49) 且其后不得紧跟数字
|
||
# (拒绝 12/01)。返回 1=ok==1;其它一切情况(ok:0 / 空行 / 非 JSON / ok 缺失 /
|
||
# 值带引号 / 非数字 / 负数)返回 0。
|
||
def ParseOkOne(Buf: bytes, Len: int) -> int:
|
||
if Buf is None or Len <= 0:
|
||
return 0
|
||
ub: BYTEPTR = BYTEPTR(Buf)
|
||
for i in range(Len):
|
||
if int(ub[i]) == 34: # '"'
|
||
KeyPos: int = 0
|
||
if i == 0:
|
||
KeyPos = 1
|
||
else:
|
||
k: int = i - 1
|
||
while k >= 0 and (int(ub[k]) == 32 or int(ub[k]) == 9):
|
||
k -= 1
|
||
if k < 0:
|
||
KeyPos = 1
|
||
else:
|
||
pk: int = int(ub[k])
|
||
if pk == 123 or pk == 44: # '{' 或 ','
|
||
KeyPos = 1
|
||
if KeyPos != 0 and i + 3 < Len:
|
||
# 依次为 '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):
|
||
j += 1
|
||
if j < Len and int(ub[j]) == 58: # ':'
|
||
j += 1
|
||
while j < Len and (int(ub[j]) == 32 or int(ub[j]) == 9):
|
||
j += 1
|
||
if j < Len and int(ub[j]) == 49: # '1'
|
||
if j + 1 >= Len:
|
||
return 1
|
||
nx: int = int(ub[j + 1])
|
||
if nx < 48 or nx > 57: # 后随非数字 ⇒ 值恰为 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 行。
|
||
def GuardClassifyLine(Buf: bytes, Len: int) -> int:
|
||
if Buf is None or Len <= 0:
|
||
return GUARD_LINE_SUSPECT # ①(空行/无内容)
|
||
if Len > GUARD_OK_LINE_MAX:
|
||
return GUARD_LINE_SUSPECT # ③
|
||
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 # ①(全空白行)
|
||
if int(ub[i]) != 123: # '{'
|
||
return GUARD_LINE_SUSPECT # ①
|
||
j: int = i
|
||
while j < Len:
|
||
if int(ub[j]) == 125: # '}'
|
||
break
|
||
j += 1
|
||
if j >= Len:
|
||
return GUARD_LINE_SUSPECT # ②
|
||
if ParseOkOne(Buf, Len) != 0:
|
||
return GUARD_LINE_OK
|
||
return GUARD_LINE_BADOK
|
||
|
||
|
||
# 把当前这一行(Guard_Line / Guard_Fill)逐字节转发到 check 通道的 IPC 写端,并补一个 '\n'
|
||
# 作为行分隔(父进程按 '\n' 切行)。行内容与用户输入逐字节相同(含空格与大小写)。
|
||
# 写失败/无通道只记 stderr:该行本就不参与心跳判定,**不改变心跳语义**(期限继续走)。
|
||
def GuardForwardLine():
|
||
if Guard_IpcWrite is None or Guard_IpcWrite == wb.INVALID_HANDLE_VALUE:
|
||
VLog.LogStr("[confirm] 警告: 收到 check 请求但无 check 通道(未传 --ipc-write),已丢弃该行\n")
|
||
return
|
||
bw: ULONG = 0
|
||
if Guard_Fill > 0:
|
||
if wf.WriteFile(Guard_IpcWrite, Guard_Line, ULONG(Guard_Fill),
|
||
&bw, None) == 0:
|
||
VLog.LogStr("[confirm] 警告: check 请求转发失败(写管道失败),已丢弃该行\n")
|
||
return
|
||
if wf.WriteFile(Guard_IpcWrite, "\n", 1, &bw, None) == 0:
|
||
VLog.LogStr("[confirm] 警告: check 请求转发失败(写行分隔符失败),该行可能不完整\n")
|
||
|
||
|
||
# ============================================================
|
||
# 非阻塞取数(控制台 / 管道 / 重定向两路共用同一 deadline 与同一行累积)
|
||
# ============================================================
|
||
|
||
# 取剩余毫秒:GetTickCount64 差值,已过期饱和到 0(调用方据此判超时,不出现负值)。
|
||
# 上界依据:--timeout 上界 CONFIRM_TIMEOUT_MAX_MS = 86400000(VCli.vp:36 校验),
|
||
# 故 Deadline - Now < 2^32,转 ULONG 不截断。
|
||
def GuardRemainMs(Deadline: ULONGLONG) -> ULONG:
|
||
Now: ULONGLONG = wb.GetTickCount64()
|
||
if Now >= Deadline:
|
||
return 0
|
||
return ULONG(Deadline - Now)
|
||
|
||
|
||
# 把一个字节累积进行缓冲(两路唯一的累积点)。返回 1=已凑满一行,0=尚未成行。
|
||
# 行结束口径与旧 ReadConfirmLine 逐条一致:
|
||
# * '\n'(10) 结束一行且不入缓冲,其前紧邻的 '\r'(13) 去掉(重定向输入的 CRLF);
|
||
# * 缓冲写满(GUARD_LINE_CAP-1)按一行结束处理(旧实现同样返回截断行);
|
||
# * 每次结束/追加后写 NUL,Guard_Fill 为本行内容长度(供 ParseOkOne 与日志使用)。
|
||
# 控制台路径把 Enter 的 '\r' 归一成 '\n' 再送入本函数,故两种输入形态共用同一口径。
|
||
def GuardFeedByte(Ch: int) -> int:
|
||
global Guard_Fill
|
||
ub: BYTEPTR = BYTEPTR(Guard_Line) # 读取用字节视图(同 ParseOkOne/旧 ReadConfirmLine)
|
||
if Ch == 10: # '\n'
|
||
if Guard_Fill > 0 and int(ub[Guard_Fill - 1]) == 13:
|
||
Guard_Fill -= 1
|
||
Guard_Line[Guard_Fill] = 0
|
||
return 1
|
||
if Guard_Fill >= GUARD_LINE_CAP - 1:
|
||
Guard_Line[Guard_Fill] = 0
|
||
return 1
|
||
Guard_Line[Guard_Fill] = char(Ch)
|
||
Guard_Fill += 1
|
||
Guard_Line[Guard_Fill] = 0
|
||
return 0
|
||
|
||
|
||
# ---- 快修:未消费输入 FIFO 的三个小函数(两路取数共用;见模块头「快修:批量输入与可疑行」)----
|
||
|
||
# 清空 FIFO(新一轮确认开始时调用:新窗口不带上一轮的残留)。
|
||
def GuardPendReset():
|
||
global Guard_PendLen, Guard_PendPos
|
||
Guard_PendLen = 0
|
||
Guard_PendPos = 0
|
||
|
||
|
||
# 把 FIFO 里尚未消费的字节按顺序逐个喂给行缓冲。返回 1=途中凑满了一行(剩余字节留在 FIFO,
|
||
# 由下一次调用继续喂),0=FIFO 已耗尽且未成行(此时两个游标归零,FIFO 回到空且可从头部复用)。
|
||
def GuardFeedPending() -> int:
|
||
global Guard_PendLen, Guard_PendPos
|
||
ub: BYTEPTR = BYTEPTR(Guard_Pend)
|
||
while Guard_PendPos < Guard_PendLen:
|
||
Ch: int = int(ub[Guard_PendPos])
|
||
Guard_PendPos += 1
|
||
if GuardFeedByte(Ch) != 0:
|
||
return 1
|
||
Guard_PendPos = 0
|
||
Guard_PendLen = 0
|
||
return 0
|
||
|
||
|
||
# 把一个字节写入 FIFO(写入端游标 Guard_PendLen)。带边界守卫:满则丢弃该字节并记一行 stderr
|
||
# 告警,绝不越界写。按构造成立的不变量(见 GUARD_PEND_CAP 的推导),入队时 FIFO 必为空且
|
||
# 单批入队量 <= 64 < GUARD_PEND_CAP,故"满"不可达;守卫只为让将来改大批上限时退化为"丢字节"
|
||
# 而不是写穿缓冲。
|
||
def GuardPendPut(Ch: int):
|
||
global Guard_PendLen
|
||
if Guard_PendLen >= GUARD_PEND_CAP:
|
||
VLog.LogFmt1("[confirm] 警告: 未消费输入 FIFO 已满(容量 %u),丢弃该批字节\n",
|
||
sizet(GUARD_PEND_CAP))
|
||
return
|
||
Guard_Pend[Guard_PendLen] = char(Ch)
|
||
Guard_PendLen += 1
|
||
|
||
|
||
# 判定 stdin 形态:控制台 / 管道 / 其它流。
|
||
# 依据:GetFileType 返回 FILE_TYPE_CHAR 的除控制台外还有 LPT 等字符设备,
|
||
# 故再用 GetConsoleMode 确认(成功 = 该句柄是控制台输入缓冲 ⇒ 可按记录读)。
|
||
def GuardInputKind(hIn: wb.HANDLE) -> int:
|
||
T: ULONG = wf.GetFileType(hIn)
|
||
if T == ULONG(FILE_TYPE_CHAR):
|
||
Mode: ULONG = 0
|
||
if wc.GetConsoleMode(hIn, &Mode) != 0:
|
||
return GUARD_IN_CONSOLE
|
||
return GUARD_IN_STREAM
|
||
if T == ULONG(FILE_TYPE_PIPE):
|
||
return GUARD_IN_PIPE
|
||
# FILE_TYPE_DISK(`< file` 重定向)/ FILE_TYPE_UNKNOWN(NUL 等)等一律按"其它流"处理
|
||
return GUARD_IN_STREAM
|
||
|
||
|
||
# ============================================================
|
||
# 加固 1: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"。
|
||
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 # 先按"不可用"记:任何一步失败都不再重试
|
||
Hm: wb.HANDLE = wb.GetModuleHandleA("kernel32.dll")
|
||
if Hm is None or Hm == wb.INVALID_HANDLE_VALUE:
|
||
return 0
|
||
Fp: VOIDPTR = wb.GetProcAddress(Hm, "ReadConsoleInputExA")
|
||
if Fp is None:
|
||
return 0
|
||
Guard_ReadEx = Fp
|
||
Guard_ReadExState = GUARD_EX_OK
|
||
return 1
|
||
|
||
|
||
# ============================================================
|
||
# 加固 2:控制事件处理例程(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 这个裸名)
|
||
def GuardCtrlHandler(CtrlType: ULONG) -> BOOL:
|
||
if Guard_RawActive == 0:
|
||
return 0
|
||
if Guard_ConHandle is not None:
|
||
wc.SetConsoleMode(Guard_ConHandle, Guard_SavedMode)
|
||
return 0
|
||
|
||
|
||
# 摘除控制事件处理例程(幂等;未注册时一次 API 都不调用)。
|
||
def GuardRemoveCtrlHandler():
|
||
global Guard_CtrlHandlerOn
|
||
if Guard_CtrlHandlerOn == 0:
|
||
return
|
||
SetConsoleCtrlHandler(GuardCtrlHandler, GUARD_CTRL_REMOVE)
|
||
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"的窗口。
|
||
def GuardEnterRaw(hIn: wb.HANDLE) -> int:
|
||
global Guard_SavedMode, Guard_RawActive, Guard_ConHandle, Guard_CtrlHandlerOn
|
||
Mode: ULONG = 0
|
||
if wc.GetConsoleMode(hIn, &Mode) == 0:
|
||
return 0
|
||
Guard_SavedMode = Mode
|
||
Guard_RawActive = 0
|
||
New: ULONG = Mode & ULONG(GUARD_RAW_CLEAR_MASK)
|
||
if New == Mode:
|
||
return 1 # 无需改模式 ⇒ 也无需 handler
|
||
Guard_ConHandle = hIn
|
||
Guard_RawActive = 1
|
||
if SetConsoleCtrlHandler(GuardCtrlHandler, GUARD_CTRL_ADD) != 0:
|
||
Guard_CtrlHandlerOn = 1
|
||
else:
|
||
VLog.LogStr("[confirm] 警告: SetConsoleCtrlHandler 注册失败,Ctrl+C 时该终端可能残留已清行输入的模式\n")
|
||
if wc.SetConsoleMode(hIn, New) == 0:
|
||
# 先清 Guard_RawActive 再清 Guard_SavedMode:handler 只在 RawActive!=0 时写回,
|
||
# 这个顺序保证它绝不会把"清零后的 SavedMode(=0)"写进控制台(此刻模式其实没变)。
|
||
Guard_RawActive = 0
|
||
Guard_SavedMode = 0
|
||
GuardRemoveCtrlHandler()
|
||
return 0
|
||
return 1
|
||
|
||
|
||
# 写回进入前的控制台模式(幂等;未实际切换过则一次 API 都不调用)。
|
||
# 顺序:写回模式 → 清 Guard_RawActive → 摘除 handler。这个顺序保证任何时刻都不存在
|
||
# "已清行输入但没有 handler"的窗口(摘除后已无待恢复的状态);反序则会在两步之间留一个
|
||
# 这样的窗口。注册与"已切换模式"同生命周期 ⇒ Guard_RawActive==0 ⇔ 未注册(无泄漏)。
|
||
def GuardLeaveRaw(hIn: wb.HANDLE):
|
||
global Guard_RawActive
|
||
if Guard_RawActive == 0:
|
||
return
|
||
wc.SetConsoleMode(hIn, Guard_SavedMode)
|
||
Guard_RawActive = 0
|
||
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 上进入阻塞读)。
|
||
def GuardPumpConsole(hIn: wb.HANDLE, Deadline: ULONGLONG) -> int:
|
||
if GuardFeedPending() != 0: # ① FIFO 里已有整行 ⇒ 直接交回,不发起读取
|
||
return 1
|
||
Read: ULONG = 0
|
||
if GuardResolveReadEx() != 0:
|
||
# Guard_ReadEx 为模块级 t.Callable,其返回类型签名已随模块级 AnnAssign 登记
|
||
# (TranslatorGenMixin.handle_module_level_var),故可直接间接调用。
|
||
if Guard_ReadEx(hIn, t.CPtr(Guard_Recs), ULONG(GUARD_REC_MAX),
|
||
&Read, USHORT(CONSOLE_READ_NOWAIT)) == 0:
|
||
return -1
|
||
else:
|
||
Rem: ULONG = GuardRemainMs(Deadline)
|
||
if Rem == 0:
|
||
return 0
|
||
if Rem > ULONG(GUARD_LEGACY_WAIT_MS):
|
||
Rem = ULONG(GUARD_LEGACY_WAIT_MS)
|
||
if wsync.WaitForSingleObject(hIn, Rem) == ULONG(wb.WAIT_FAILED):
|
||
return -1
|
||
Cnt: ULONG = 0
|
||
if wc.GetNumberOfConsoleInputEvents(hIn, &Cnt) == 0:
|
||
return -1
|
||
if Cnt == 0:
|
||
return 0
|
||
N: ULONG = Cnt
|
||
if N > ULONG(GUARD_REC_MAX):
|
||
N = ULONG(GUARD_REC_MAX)
|
||
if wc.ReadConsoleInputA(hIn, t.CPtr(Guard_Recs), N, &Read) == 0:
|
||
return -1
|
||
ub: BYTEPTR = BYTEPTR(Guard_Recs)
|
||
i: int = 0
|
||
while i < int(Read):
|
||
Base: int = i * GUARD_REC_SIZE
|
||
i += 1
|
||
if VPe.ReadU16(ub, Base) != uint32(wc.KEY_EVENT):
|
||
continue
|
||
if VPe.ReadU32(ub, Base + GUARD_KEY_DOWN_OFF) == 0:
|
||
continue
|
||
Ch: int = int(VPe.ReadU16(ub, Base + GUARD_KEY_CHAR_OFF))
|
||
if Ch == 13:
|
||
Ch = 10 # Enter → 行结束(与 '\n' 同一口径)
|
||
elif Ch != 9 and (Ch < 32 or Ch > 126):
|
||
continue # 非可打印/非 ASCII:忽略
|
||
GuardPendPut(Ch) # ③ 过滤后压入 FIFO(本次调用至多 16 字节)
|
||
if GuardFeedPending() != 0: # ③ 再喂 FIFO;剩余字节留给下次调用
|
||
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 字节)。
|
||
def GuardPumpStream(hIn: wb.HANDLE, Kind: int) -> int:
|
||
if GuardFeedPending() != 0: # ① FIFO 里已有整行 ⇒ 直接交回,不发起读取
|
||
return 1
|
||
N: ULONG = ULONG(GUARD_CHUNK_CAP)
|
||
if Kind == GUARD_IN_PIPE:
|
||
Avail: ULONG = 0
|
||
if PeekNamedPipe(hIn, t.CPtr(0), 0, t.CPtr(0), &Avail, t.CPtr(0)) == 0:
|
||
return -1
|
||
if Avail == 0:
|
||
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。
|
||
if N > ULONG(GUARD_CHUNK_CAP):
|
||
N = ULONG(GUARD_CHUNK_CAP)
|
||
if N > ULONG(GUARD_PEND_CAP):
|
||
N = ULONG(GUARD_PEND_CAP)
|
||
Br: ULONG = 0
|
||
if wf.ReadFile(hIn, Guard_Chunk, N, &Br, None) == 0:
|
||
return -1
|
||
if Br == 0:
|
||
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;剩余字节留给下次调用
|
||
return 1
|
||
return 0
|
||
|
||
|
||
# ============================================================
|
||
# 看门狗侧:失败处理 + 主循环
|
||
# ============================================================
|
||
|
||
# 失败路径:stderr 打明原因 → stdout 逐字输出唯一一行 {"exit":1, "msg": "timeout/ok!=1"}\n
|
||
# → TerminateProcess(父进程) → sys.exit(1)。
|
||
# 父进程被强杀(TerminateProcess 不做任何收尾):不写缓存、不再打本轮数组 JSON。
|
||
def GuardWatchFail(hParent: wb.HANDLE, Reason: str):
|
||
if Reason is not None:
|
||
VLog.LogFmtS1("[confirm] 失败: %s\n", Reason)
|
||
VLog.StdoutWrite("{\"exit\":1, \"msg\": \"timeout/ok!=1\"}\n")
|
||
if hParent is not None and hParent != wb.INVALID_HANDLE_VALUE:
|
||
wp.TerminateProcess(hParent, 1)
|
||
wb.CloseHandle(hParent)
|
||
sys.exit(1)
|
||
|
||
|
||
# 看门狗侧缓冲一次性分配;返回 1=可用,0=失败
|
||
def GuardWatchInit() -> int:
|
||
global Guard_Line, Guard_Stamp, Guard_Dl, Guard_Recs, Guard_Chunk, Guard_Pend
|
||
if Guard_Line is None:
|
||
Guard_Line = _mbuddy.alloc(GUARD_LINE_CAP)
|
||
Guard_Stamp = _mbuddy.alloc(GUARD_STAMP_CAP)
|
||
Guard_Dl = _mbuddy.alloc(GUARD_STAMP_CAP)
|
||
Guard_Recs = _mbuddy.alloc(GUARD_REC_CAP)
|
||
Guard_Chunk = _mbuddy.alloc(GUARD_CHUNK_CAP)
|
||
Guard_Pend = _mbuddy.alloc(GUARD_PEND_CAP)
|
||
if Guard_Line is None or Guard_Stamp is None or Guard_Dl is None:
|
||
return 0
|
||
if Guard_Recs is None or Guard_Chunk is None or Guard_Pend is None:
|
||
return 0
|
||
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} 才判失败"对"无输入"与"只有被忽略的行"一致成立)。
|
||
def GuardWaitConfirm(hIn: wb.HANDLE, hParent: wb.HANDLE, Kind: int, TimeoutMs: int) -> int:
|
||
global Guard_Fill
|
||
GuardPendReset() # ① 新一轮:FIFO 与行缓冲都不带上一轮残留
|
||
Guard_Fill = 0
|
||
Guard_Line[0] = 0
|
||
if Kind == GUARD_IN_CONSOLE:
|
||
# 清 ENABLE_LINE_INPUT:清掉后回车不再由系统"行输入"消化,而以 KEY_EVENT 到达,
|
||
# 我们才能自己判"一整行"(依据见模块头"控制台模式取舍")。
|
||
# 拿不到/改不了控制台模式 ⇒ 无法保证"timeout 内可判定"(旧行为是 ReadFile 等到回车),
|
||
# 故按既有约定失败收尾,不静默降级为会挂死的阻塞等待。
|
||
if GuardEnterRaw(hIn) == 0:
|
||
GuardWatchFail(hParent, "控制台模式读取/切换失败")
|
||
return 0
|
||
Deadline: ULONGLONG = wb.GetTickCount64() + ULONGLONG(uint32(TimeoutMs))
|
||
while True:
|
||
# ② 先取数(GuardPump* 内部先喂 FIFO):FIFO 里已有整行时不会白等一次 20ms
|
||
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 口径不变)。
|
||
if VCheck.ParseCheckLine(str(Guard_Line)) != 0:
|
||
GuardForwardLine()
|
||
else:
|
||
Cls: int = GuardClassifyLine(Guard_Line, Guard_Fill)
|
||
if Cls == GUARD_LINE_OK:
|
||
break # 合法 {"ok": 1} ⇒ 出循环后收尾成功
|
||
if Cls == GUARD_LINE_BADOK:
|
||
# 形状合法但值不对("用户真的输错")⇒ 与旧实现同一失败口径与同一 stderr 文案
|
||
GuardLeaveRaw(hIn)
|
||
VLog.LogFmtS1("[confirm] 收到非 {\"ok\": 1} 的输入: %s\n", str(Guard_Line))
|
||
GuardWatchFail(hParent, "ok != 1")
|
||
return 0
|
||
# 形状可疑(首字符非 '{' / 不含 '}' / 长度异常,含空行)⇒ 忽略该行并回显(截断)
|
||
if Guard_Fill > GUARD_SUSPECT_ECHO_MAX:
|
||
Guard_Line[GUARD_SUSPECT_ECHO_MAX] = 0 # 只回显前缀(该行随后整个丢弃)
|
||
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" 分支不会命中,控制流直接进入等待段。
|
||
if R < 0:
|
||
GuardLeaveRaw(hIn)
|
||
if R == GUARD_PUMP_EOF:
|
||
GuardWatchFail(hParent, "stdin 已到 EOF,不可能再有输入")
|
||
else:
|
||
GuardWatchFail(hParent, "读取 stdin 失败")
|
||
return 0
|
||
# ③ 本轮取数无果 ⇒ 等"stdin 可读 / 父进程退出 / 到点",上限 = min(剩余, GUARD_POLL_MS)
|
||
Rem2: ULONG = GuardRemainMs(Deadline)
|
||
if Rem2 == 0:
|
||
GuardLeaveRaw(hIn)
|
||
GuardWatchFail(hParent, "超时无输入")
|
||
return 0
|
||
Sl: ULONG = Rem2
|
||
if Sl > ULONG(GUARD_POLL_MS):
|
||
Sl = ULONG(GUARD_POLL_MS)
|
||
Hs: WATCH_HANDLES = WATCH_HANDLES()
|
||
Hs.H0 = hIn
|
||
Hs.H1 = hParent
|
||
Wm: ULONG = wsync.WaitForMultipleObjects(ULONG(WATCH_HANDLE_COUNT),
|
||
&Hs, 0, Sl)
|
||
if Wm == ULONG(wb.WAIT_OBJECT_0) + ULONG(1): # 父进程句柄(索引 1)置信号
|
||
GuardLeaveRaw(hIn)
|
||
return 0
|
||
if Wm == ULONG(wb.WAIT_FAILED):
|
||
GuardLeaveRaw(hIn)
|
||
GuardWatchFail(hParent, "等待输入失败")
|
||
return 0
|
||
# 其余取值(stdin 置信号 / WAIT_TIMEOUT)⇒ 回到循环顶部取数
|
||
GuardLeaveRaw(hIn)
|
||
return 1
|
||
|
||
|
||
# confirm-watch 入口(App/main.vp 分派)。返回进程退出码。
|
||
# 只做心跳确认,不碰扫盘/目录表/缓存。
|
||
def RunConfirmWatch() -> int:
|
||
global Guard_IpcWrite
|
||
if VCli.ParseWatchArgs() != 0:
|
||
return 1
|
||
# 详细度由父进程经 --verbose 透传(父为 --verbose 时 SpawnWatchdog 会追加该开关)
|
||
VLog.SetVerbose(VCli.WatchVerbose())
|
||
IntervalSec: int = VCli.WatchInterval()
|
||
TimeoutMs: int = VCli.WatchTimeout()
|
||
ParentPid: LONGLONG = VCli.WatchParentPid()
|
||
IntervalMs: ULONG = ULONG(IntervalSec) * ULONG(1000)
|
||
if GuardWatchInit() == 0:
|
||
VLog.LogStr("[confirm] 看门狗: 行/记录/时间戳/未消费输入缓冲分配失败\n")
|
||
return 1
|
||
|
||
VLog.LogFmt3("[confirm] 看门狗启动: 每 %u 秒要求一次 {\"ok\": 1},超时 %u ms,父进程 pid=%u\n",
|
||
sizet(IntervalSec), sizet(TimeoutMs), sizet(ParentPid))
|
||
|
||
# 父进程句柄:同时用于"父退出检测(SYNCHRONIZE)"与"失败时终止父进程(PROCESS_TERMINATE)"
|
||
hParent: wb.HANDLE = wp.OpenProcess(ULONG(WATCH_PARENT_ACCESS), 0, ULONG(ParentPid))
|
||
if hParent is None or hParent == wb.INVALID_HANDLE_VALUE:
|
||
# 拿不到父进程句柄 ⇒ 既无法监控也无法终止,无法履行契约:只记 stderr 并以 1 退出
|
||
VLog.LogFmt1("[confirm] 看门狗: OpenProcess 父进程失败 (GetLastError=%u),自身退出\n",
|
||
sizet(wb.GetLastError()))
|
||
return 1
|
||
|
||
hIn: wb.HANDLE = wf.GetStdHandle(wf.STD_INPUT_HANDLE)
|
||
if hIn is None or hIn == wb.INVALID_HANDLE_VALUE:
|
||
GuardWatchFail(hParent, "stdin 句柄无效")
|
||
return 1
|
||
|
||
# check 通道写端:父进程经 `--ipc-write <数值>` 传入的**句柄数值**,这里还原成 HANDLE
|
||
# (数值按 u64 位模式传递与解析;0/缺参 ⇒ 无通道,收到 check 行时只记 stderr)。
|
||
if VCli.WatchIpcWrite() != 0:
|
||
Guard_IpcWrite = t.CPtr(VCli.WatchIpcWrite())
|
||
else:
|
||
Guard_IpcWrite = None
|
||
Ch0: int = 0
|
||
if Guard_IpcWrite is not None:
|
||
Ch0 = 1
|
||
VLog.VerboseFmt1("[confirm] 看门狗: check 通道=%u (1=已配置 0=无)\n", sizet(Ch0))
|
||
|
||
# 输入形态只判一次(句柄在进程生命周期内不变);控制台与管道/重定向的取数路径不同。
|
||
Kind: int = GuardInputKind(hIn)
|
||
VLog.VerboseFmt1("[confirm] 看门狗: stdin 形态=%u (1=控制台 2=管道 3=其它流)\n",
|
||
sizet(Kind))
|
||
# 控制台取数路径(加固 1):主路径=ReadConsoleInputExA+CONSOLE_READ_NOWAIT,
|
||
# 降级=GetNumberOfConsoleInputEvents+ReadConsoleInputA(该导出不存在的老系统)。
|
||
# 这里提前解析一次(后续判定复用缓存的状态),顺带把实际走哪一路写进 --verbose 日志。
|
||
if Kind == GUARD_IN_CONSOLE:
|
||
GuardResolveReadEx()
|
||
VLog.VerboseFmt1("[confirm] 看门狗: 控制台取数路径=%u (1=ReadConsoleInputExA+NOWAIT 2=降级查询+ReadConsoleInputA)\n",
|
||
sizet(Guard_ReadExState))
|
||
|
||
while True:
|
||
# 阶段 1:等 interval;父进程先退出则立即自退(无需父侧配合,杜绝孤儿)
|
||
Ws: ULONG = wsync.WaitForSingleObject(hParent, IntervalMs)
|
||
if Ws == wb.WAIT_OBJECT_0:
|
||
VLog.LogStr("[confirm] 看门狗: 父进程已退出,自身退出\n")
|
||
wb.CloseHandle(hParent)
|
||
return 0
|
||
if Ws != wb.WAIT_TIMEOUT:
|
||
VLog.LogFmt1("[confirm] 看门狗: 等待父进程失败 (wait=%u)\n", sizet(Ws))
|
||
GuardWatchFail(hParent, "等待父进程失败")
|
||
return 1
|
||
|
||
# 阶段 2:stderr 提示(期限自本行起算)+ 在 timeout 内非阻塞等"完整一行且分类通过"
|
||
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)。
|
||
if GuardWaitConfirm(hIn, hParent, Kind, TimeoutMs) == 0:
|
||
VLog.LogStr("[confirm] 看门狗: 父进程已退出,自身退出\n")
|
||
wb.CloseHandle(hParent)
|
||
return 0
|
||
VLog.LogFmt1("[confirm] 确认成功 (ok=1),下一次约 %u 秒后\n", sizet(IntervalSec))
|