490 lines
24 KiB
Plaintext
490 lines
24 KiB
Plaintext
import stdlib
|
|
import string
|
|
import viperlib
|
|
import memhub
|
|
import json
|
|
import lib.VFs as VFs
|
|
import lib.VLog as VLog
|
|
import lib.VPe as VPe
|
|
import lib.VProc as VProc
|
|
import lib.VGuid as VGuid
|
|
import lib.VCache as VCache
|
|
|
|
|
|
# ============================================================
|
|
# VCheck - execution + reply side of the check request channel (in the parent / scan process)
|
|
#
|
|
# Channel (attached to --need-confirm; present only while the watchdog runs):
|
|
# * A caller writes one line to stdin: {"check": "<exe path>", "id": <uint64>}
|
|
# * The watchdog parses it with the repo json library; a string "check" plus an integer "id" marks
|
|
# a check request, forwarded byte-for-byte (plus '\n') to the IPC pipe; it is not a heartbeat.
|
|
# * The parent polls that pipe without blocking (VGuard.PollIpc) and hands each line to this module.
|
|
# * This module runs the check and writes a single-line JSON reply to stdout:
|
|
# ok {"check_result": true, "id": <id>, "viper": {..}} (no check_msg)
|
|
# fail {"check_result": false, "id": <id>, "check_msg": "<reason>"}
|
|
# Serialized by the repo json library (compact single line, key order = insertion order).
|
|
#
|
|
# 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")
|
|
#
|
|
# Failure reasons are only the fixed ASCII strings below (case-sensitive): NOT_FOUND, NO_PERMISSION,
|
|
# NOT_EXE, NO_PRVERSN, PRVERSN_ERR, PREFIX_ERR, "EXE_ERR,0x<HEX>" (from VProc.ProbeLaunchErr),
|
|
# TIMEOUT, and CHANNEL_UNAVAILABLE (this module's buffers / reply pool unavailable, CheckInit failed;
|
|
# see EmitReplyUnavailable). Exact conditions are documented on the CK_* constants.
|
|
#
|
|
# 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.
|
|
#
|
|
# Revalidation (VScan.RevalidateDiscovered) reuses VerifyExecutable (same rule); failures are removed.
|
|
#
|
|
# 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).
|
|
#
|
|
# 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.
|
|
#
|
|
# Injection: _mbuddy must be the same MemBuddy instance injected by App/main.vp; otherwise all
|
|
# allocations return None and CheckInit fails.
|
|
# ============================================================
|
|
|
|
# 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 launched but the first 5 chars are not Viper
|
|
define CK_TIMEOUT = "TIMEOUT"
|
|
# channel itself unavailable (CheckInit failed): the only degradation reason, see EmitReplyUnavailable
|
|
define CK_CHANNEL_UNAVAILABLE = "CHANNEL_UNAVAILABLE"
|
|
|
|
# source value when a check hit is cached (distinct from the scan side's "workspace")
|
|
define CK_SOURCE = "check"
|
|
|
|
# EXE_ERR reason buffer ("EXE_ERR,0x" + up to 8 hex digits + NUL)
|
|
define CK_MSG_CAP = 32
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# Reply pool: 72 KiB. See CK_REPLY_NEED_FIXED for the per-reply allocation bound.
|
|
define CK_REPLY_POOL_CAP = 73728
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# injected by App/main.vp (same MemBuddy instance as other modules)
|
|
_mbuddy: memhub.MemBuddy*
|
|
|
|
# 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 = handling a check (re-entrancy guard; PollIpc is called from several scan points)
|
|
|
|
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 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
|
|
# ============================================================
|
|
|
|
# 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:
|
|
Check_Path = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
|
if Check_Path is None:
|
|
return 0
|
|
return 1
|
|
|
|
|
|
# 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
|
|
global Check_Id, Check_Disp, Check_Conv, Check_Arena, Check_Pool
|
|
if Check_Tried != 0:
|
|
return Check_Ready
|
|
Check_Tried = 1
|
|
Check_Ver = _mbuddy.alloc(VPe.VER_CAP)
|
|
Check_Arch = _mbuddy.alloc(VPe.ARCH_CAP)
|
|
Check_Probe = _mbuddy.alloc(VProc.VERSION_PROBE_DIAG_CAP)
|
|
Check_Msg = _mbuddy.alloc(CK_MSG_CAP)
|
|
Check_Id = _mbuddy.alloc(VGuid.GUID_STR_CAP)
|
|
Check_Disp = _mbuddy.alloc(VCache.DISP_CAP)
|
|
Check_Conv = _mbuddy.alloc(VFs.CONV_CAP)
|
|
Check_Arena = stdlib.malloc(CK_REPLY_POOL_CAP)
|
|
if EnsurePathBuf() == 0:
|
|
VLog.LogStr(" Warning: check buffer allocation failed (request path buffer), check channel unavailable\n")
|
|
return 0
|
|
if Check_Ver is None or Check_Arch is None or Check_Probe is None:
|
|
VLog.LogStr(" Warning: check buffer allocation failed (version/arch/probe buffer), check channel unavailable\n")
|
|
return 0
|
|
if Check_Msg is None or Check_Id is None:
|
|
VLog.LogStr(" Warning: check buffer allocation failed (reason string/UUID buffer), check channel unavailable\n")
|
|
return 0
|
|
if Check_Disp is None or Check_Conv is None or Check_Arena is None:
|
|
VLog.LogStr(" Warning: check buffer allocation failed (display string/conversion buffer/reply arena), check channel unavailable\n")
|
|
return 0
|
|
# 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(" Warning: check reply pool construction failed, check channel unavailable\n")
|
|
return 0
|
|
Check_Pool = Pool
|
|
Check_Ready = 1
|
|
return 1
|
|
|
|
|
|
# ============================================================
|
|
# request-line parsing (shared rule for the watchdog classification and the parent execution)
|
|
# ============================================================
|
|
|
|
# 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
|
|
Req_PathOk = 0
|
|
if Txt is None:
|
|
return 0
|
|
L: sizet = string.strlen(Txt)
|
|
if L == 0 or L > sizet(CK_LINE_MAX):
|
|
return 0
|
|
Cap: sizet = L * sizet(CK_ARENA_PER_BYTE) + sizet(CK_ARENA_SLACK)
|
|
Arena: bytes = stdlib.malloc(Cap)
|
|
if Arena is None:
|
|
return 0
|
|
Pool: memhub.MemPool* = memhub.MemPool(Arena, Cap)
|
|
if Pool is None:
|
|
stdlib.free(rawptr(Arena))
|
|
return 0
|
|
# 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(rawptr(Arena))
|
|
return 0
|
|
Root: json.JsonValue* = json.parse(Pool, Txt)
|
|
HasCk: int = 0
|
|
HasId: int = 0
|
|
if Root != None and Root.vtype == json.JSON_OBJECT:
|
|
cu: json.JsonValue* = Root.child
|
|
while cu != None:
|
|
if cu.key != None:
|
|
if string.strcmp(cu.key, "check") == 0:
|
|
if cu.vtype == json.JSON_STRING and cu.str_val != None:
|
|
if string.strlen(cu.str_val) > 0:
|
|
HasCk = 1
|
|
# 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
|
|
elif string.strcmp(cu.key, "id") == 0:
|
|
if cu.vtype == json.JSON_INT:
|
|
Req_Id = longlong(cu.int_val)
|
|
HasId = 1
|
|
cu = cu.next
|
|
stdlib.free(rawptr(Arena))
|
|
if HasCk != 0 and HasId != 0:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
# 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
|
|
# ============================================================
|
|
|
|
# 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:
|
|
return CK_NOT_FOUND
|
|
return CK_NO_PERMISSION
|
|
if Rc == 5 or Rc == 107:
|
|
return CK_NO_PRVERSN
|
|
if Rc == 6 or Rc == 7:
|
|
return CK_PRVERSN_ERR
|
|
if Rc >= 100:
|
|
prc: int = Rc - 100
|
|
if prc == 7:
|
|
return CK_NO_PRVERSN
|
|
if prc == 6:
|
|
return CK_PRVERSN_ERR
|
|
return CK_NOT_EXE
|
|
return CK_NOT_EXE
|
|
|
|
|
|
# Format a launch-failure code as `EXE_ERR,0x<UPPERHEX>` (writes Check_Msg, returns it), e.g.
|
|
# 2 -> "EXE_ERR,0x2", 193 -> "EXE_ERR,0xC1". The result points to a module buffer and must be used
|
|
# immediately (EmitReplyFail copies it into the reply pool).
|
|
def FormatExeErr(Err: ulong) -> str:
|
|
viperlib.snprintf(Check_Msg, sizet(CK_MSG_CAP), "EXE_ERR,0x%X", Err)
|
|
return str(Check_Msg)
|
|
|
|
|
|
# ============================================================
|
|
# check execution
|
|
# ============================================================
|
|
|
|
# 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
|
|
# (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 # 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:
|
|
# launch failed (args/command buffer/pipe/CreateProcessA): report EXE_ERR,0x<code>
|
|
return FormatExeErr(VProc.ProbeLaunchErr())
|
|
if VProc.OutputHasViperPrefix(str(Check_Probe)) == 0:
|
|
# 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
|
|
|
|
|
|
# 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
|
|
if CheckExecutable(PathAnsi) == None:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
# ============================================================
|
|
# cache insert (check hit)
|
|
# ============================================================
|
|
|
|
# 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
|
|
if VCache.CacheAdd(PathAnsi, str(Check_Id), str(Check_Ver), str(Check_Arch),
|
|
CK_SOURCE, str(Check_Disp)) == 0:
|
|
VLog.LogStr(" Warning: check hit but cache insert failed (capacity full or allocation failed), entry not tracked this round\n")
|
|
return 0
|
|
return 1
|
|
|
|
|
|
# ============================================================
|
|
# reply (single-line JSON on stdout)
|
|
# ============================================================
|
|
|
|
# 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):
|
|
Txt[n] = '\n'
|
|
Txt[n + 1] = 0
|
|
VLog.StdoutWrite(Txt)
|
|
return 1
|
|
# 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
|
|
|
|
|
|
# Success reply: {"check_result": true, "id": <id>, "viper": {<found item>}}. The viper dict has the
|
|
# same shape/order as existing found items (id/executable/version/arch/source/displayName); source
|
|
# is "check". A cached exe reuses its cache fields (id stable, nothing rewritten).
|
|
def EmitReplyOk(ReqIdVal: longlong, PathAnsi: str):
|
|
Id: str = str(Check_Id)
|
|
Ver: str = str(Check_Ver)
|
|
Arch: str = str(Check_Arch)
|
|
Disp: str = str(Check_Disp)
|
|
idx: int = VCache.CacheFind(PathAnsi)
|
|
if idx >= 0:
|
|
# 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:
|
|
# 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(" Warning: CoCreateGuid failed, id of the check cache entry is empty\n")
|
|
Check_Id[0] = 0
|
|
Id = str(Check_Id)
|
|
TrackChecked(PathAnsi)
|
|
Check_Pool.reset()
|
|
ValNeed: sizet = sizet(CK_CONV_MAX) + sizet(CK_CONV_MAX)
|
|
ValNeed = ValNeed + string.strlen(Id) + string.strlen(Ver) + string.strlen(Arch)
|
|
ValNeed = ValNeed + string.strlen(CK_SOURCE)
|
|
if Check_Pool.AllocOk(sizet(CK_REPLY_NEED_FIXED) + ValNeed) == 0:
|
|
VLog.LogStr(" Warning: check reply pool insufficient, not replied (buffer will not be overrun)\n")
|
|
return
|
|
Obj: json.JsonValue* = json.object(Check_Pool)
|
|
if Obj == None:
|
|
VLog.LogStr(" Warning: check reply object allocation failed, not replied\n")
|
|
return
|
|
json.object_set(Check_Pool, Obj, "check_result", json.bool_val(Check_Pool, True))
|
|
json.object_set(Check_Pool, Obj, "id", json.int_val(Check_Pool, int64(ReqIdVal)))
|
|
Sub: json.JsonValue* = json.object(Check_Pool)
|
|
if Sub == None:
|
|
VLog.LogStr(" Warning: check reply object allocation failed, not replied\n")
|
|
return
|
|
json.object_set(Check_Pool, Sub, "id", json.string_val(Check_Pool, Id))
|
|
# 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)))
|
|
json.object_set(Check_Pool, Sub, "version", json.string_val(Check_Pool, Ver))
|
|
json.object_set(Check_Pool, Sub, "arch", json.string_val(Check_Pool, Arch))
|
|
json.object_set(Check_Pool, Sub, "source", json.string_val(Check_Pool, CK_SOURCE))
|
|
if VFs.AnsiToUtf8(Disp, Check_Conv, VFs.CONV_CAP) == 0:
|
|
VFs.CopyRaw(Check_Conv, VFs.CONV_CAP, Disp)
|
|
json.object_set(Check_Pool, Sub, "displayName", json.string_val(Check_Pool, str(Check_Conv)))
|
|
json.object_set(Check_Pool, Obj, "viper", Sub)
|
|
Txt: str = json.write(Check_Pool, Obj, False)
|
|
if Txt == None:
|
|
VLog.LogStr(" Warning: check reply serialization failed (pool insufficient), not replied\n")
|
|
return
|
|
WriteReplyLine(Txt)
|
|
|
|
|
|
# Failure reply: {"check_result": false, "id": <id>, "check_msg": "<reason>"}
|
|
def EmitReplyFail(ReqIdVal: longlong, Msg: str):
|
|
Check_Pool.reset()
|
|
Need: sizet = sizet(CK_REPLY_NEED_FIXED)
|
|
if Msg != None:
|
|
Need = Need + string.strlen(Msg)
|
|
if Check_Pool.AllocOk(Need) == 0:
|
|
VLog.LogStr(" Warning: check reply pool insufficient, not replied (buffer will not be overrun)\n")
|
|
return
|
|
Obj: json.JsonValue* = json.object(Check_Pool)
|
|
if Obj == None:
|
|
VLog.LogStr(" Warning: check reply object allocation failed, not replied\n")
|
|
return
|
|
json.object_set(Check_Pool, Obj, "check_result", json.bool_val(Check_Pool, False))
|
|
json.object_set(Check_Pool, Obj, "id", json.int_val(Check_Pool, int64(ReqIdVal)))
|
|
json.object_set(Check_Pool, Obj, "check_msg", json.string_val(Check_Pool, Msg))
|
|
Txt: str = json.write(Check_Pool, Obj, False)
|
|
if Txt == None:
|
|
VLog.LogStr(" Warning: check reply serialization failed (pool insufficient), not replied\n")
|
|
return
|
|
WriteReplyLine(Txt)
|
|
|
|
|
|
# Degradation reply (compact single line): {"check_result": false, "id": <id>,
|
|
# "check_msg": "CHANNEL_UNAVAILABLE"}. Used only when CheckInit failed, so it must not depend on
|
|
# _mbuddy / Check_Pool: the line buffer comes from stdlib.malloc (freed immediately), and the body
|
|
# is produced by viperlib.snprintf with a fixed template matching the library's compact form (no
|
|
# escaping needed). This keeps "request received but channel unavailable" visible. Returns 1 =
|
|
# replied, 0 = even the temp buffer is unavailable.
|
|
def EmitReplyUnavailable(ReqIdVal: longlong) -> int:
|
|
Fb: bytes = stdlib.malloc(CK_FALLBACK_CAP)
|
|
if Fb is None:
|
|
return 0
|
|
viperlib.snprintf(Fb, sizet(CK_FALLBACK_CAP),
|
|
"{\"check_result\":false,\"id\":%lld,\"check_msg\":\"%s\"}\n",
|
|
int64(ReqIdVal), CK_CHANNEL_UNAVAILABLE)
|
|
VLog.StdoutWrite(str(Fb))
|
|
stdlib.free(rawptr(Fb))
|
|
return 1
|
|
|
|
|
|
# ============================================================
|
|
# full handling of one request line (called by VGuard.PollIpc at a non-blocking poll point)
|
|
# ============================================================
|
|
|
|
# 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:
|
|
return 0
|
|
if CheckInit() == 0:
|
|
if ParseCheckLine(Txt) == 0:
|
|
return 0
|
|
EmitReplyUnavailable(Req_Id)
|
|
return 1
|
|
if ParseCheckLine(Txt) == 0:
|
|
return 0
|
|
Check_Handling = 1
|
|
Path: str = str(Check_Path)
|
|
Reason: str = CheckExecutable(Path)
|
|
if Reason == None:
|
|
EmitReplyOk(Req_Id, Path)
|
|
else:
|
|
EmitReplyFail(Req_Id, Reason)
|
|
Check_Handling = 0
|
|
return 1
|