Files
2026-10-17 21:57:25 +08:00

461 lines
21 KiB
Plaintext

import string
import sys
import lib.VLog as VLog
import lib.VDirs as VDirs
# 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 seconds upper bound (1 day; over = argument error)
define DEFAULT_SLEEP_MS = 10000 # --sleep default (ms)
define SLEEP_MAX_MS = 86400000 # --sleep ms upper bound (1 day; over = argument error)
define DEFAULT_CONFIRM_TIMEOUT_MS = 5000 # --need-confirm-timeout default (ms)
define CONFIRM_TIMEOUT_MAX_MS = 86400000 # --need-confirm-timeout ms upper bound (1 day)
# --pace <ms>: throttle sleep duration, range 0..60000; 0 = disable throttling (no Sleep).
# Upper bound is 1 minute (its purpose differs from --sleep's 1-day bound).
define DEFAULT_PACE_SLEEP_MS = 20 # --pace default (ms)
define PACE_SLEEP_MAX_MS = 60000 # --pace ms upper bound (1 minute)
# --pace-units <N>: file granularity, range 1..1000000 (a sleep point every N files; 1 = every file).
# 0 is meaningless and rejected along with invalid input; upper bound 1000000 is sparse enough.
define DEFAULT_PACE_FILE_UNITS = 64 # --pace-units default
define PACE_UNITS_MAX = 1000000 # --pace-units file-count upper bound
# --parent-pid upper bound (full DWORD range 4294967295).
# Use a wide global (longlong): a t.CDefine in u32 range materializes as i32 and truncates.
PID_MAX: longlong = 4294967295 # --parent-pid upper bound (full DWORD range)
Cache_Path: str = None
Once_Flag: int = 0
Need_Confirm: int = 0
Sleep_Ms: int = DEFAULT_SLEEP_MS
Need_Confirm_Timeout: int = DEFAULT_CONFIRM_TIMEOUT_MS
Verbose_Flag: int = 0 # --verbose (scan); 1 = 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 = print searchable paths and exit
# --pace/--pace-units (scan): explicitly given => enable throttling (also under --once).
# Pace_Set records whether either was given; otherwise VScan keeps its own defaults.
Pace_Set: int = 0 # 1 = --pace or --pace-units given on the command line
Pace_Ms: int = DEFAULT_PACE_SLEEP_MS # --pace <ms> (0 = disable throttling)
Pace_Units: int = DEFAULT_PACE_FILE_UNITS # --pace-units <N> (sleep point every N files; 1 = every file)
# --first-no-pace (scan): disable throttling in round 1 only; later rounds follow normal rules.
# Does not change Pace_Set/Pace_Ms/Pace_Units; distinct from --pace 0 (no throttle in any round).
First_No_Pace_Flag: int = 0 # 1 = round 1 runs without throttling
# confirm-watch (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 output (module global, same convention as VPe.Pe_RawPtr)
U64_Val: ulonglong = 0
# 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
def CachePath() -> str:
return Cache_Path
def Once() -> int:
return Once_Flag
def NeedConfirm() -> int:
return Need_Confirm
def SleepMs() -> int:
return Sleep_Ms
def NeedConfirmTimeout() -> int:
return Need_Confirm_Timeout
def Verbose() -> int:
return Verbose_Flag
def SearchPaths() -> int:
return Search_Paths_Flag
# --pace/--pace-units 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
def PaceSleepMs() -> int:
return Pace_Ms
def PaceFileUnits() -> int:
return Pace_Units
# --first-no-pace: 1 = round 1 runs without throttling (later rounds follow normal rules).
def FirstNoPace() -> int:
return First_No_Pace_Flag
# 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
def WatchInterval() -> int:
return Watch_Interval
def WatchTimeout() -> int:
return Watch_Timeout
def WatchParentPid() -> longlong:
return Watch_ParentPid
def WatchIpcWrite() -> ulonglong:
return Watch_IpcWrite
def WatchVerbose() -> int:
return Watch_Verbose
# ver 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]
if A is not None:
if string.strcmp(A, "--verbose") == 0:
return 1
return 0
# 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
ub: bytes = bytes(S)
if ub[0] == 0:
return -1
v: longlong = 0
i: int = 0
while ub[i] != 0:
ch: int = int(ub[i])
if ch < 48 or ch > 57:
return -1
if v > Max: # bounds short-circuit; also guards v*10 overflow
return -1
v = v * 10 + longlong(ch - 48)
if v > Max or v < 0: # out of range / 64-bit wrap (latter unreachable)
return -1
i += 1
return v
# --need-confirm seconds parse: equivalent to ParseUint(S, SECS_MAX), keeps error code -1
def ParseSecs(S: str) -> int:
return ParseUint(S, SECS_MAX)
# 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
if S is None:
return 0
ub: bytes = bytes(S)
if ub[0] == 0:
return 0
v: ulonglong = 0
i: int = 0
while ub[i] != 0:
ch: int = int(ub[i])
if ch < 48 or ch > 57:
return 0
d: ulonglong = ulonglong(ch - 48)
if v > ulonglong(1844674407370955161): # times 10 would overflow
return 0
if v == ulonglong(1844674407370955161) and d > ulonglong(5):
return 0
v = v * ulonglong(10) + d
i += 1
U64_Val = v
return 1
def PrintUsage():
VLog.LogStr("VET - Viper Environment Tool\n")
VLog.LogStr("Usage:\n")
VLog.LogStr(" VET.exe ver <exe> [--verbose]\n")
VLog.LogStr(" Validate one exe once (common directory table + .PRVERSN section + --version prefix);\n")
VLog.LogStr(" --verbose is the valueless switch shared with scan (prints the target path and parse/probe details).\n")
VLog.LogStr(" VET.exe scan --cache <path> [--once] [--sleep <ms>] [--pace <ms>] [--pace-units <N>]\n")
VLog.LogStr(" [--first-no-pace] [--need-confirm <sec> [--need-confirm-timeout <ms>]]\n")
VLog.LogStr(" [--verbose] [--path <dir>]...\n")
VLog.LogStr(" Resident disk scanner; result JSON goes to stdout, cache is written to <path>.\n")
VLog.LogStr(" VET.exe scan --search-paths [--path <dir>]... [--verbose]\n")
VLog.LogStr(" VET.exe --search-paths [--path <dir>]... [--verbose] (same as above; scan may be omitted: query only)\n")
VLog.LogStr(" --once run a single round only (for verification).\n")
VLog.LogStr(" --sleep <ms> interval per round (default 10000; 0 = no sleep; max 86400000).\n")
VLog.LogStr(" --pace <ms> sleep duration at throttle points (default 20; 0 = disable throttling; max 60000).\n")
VLog.LogStr(" --pace-units <N> one throttle point every N files (default 64; 1 = stop at every file; max 1000000).\n")
VLog.LogStr(" giving --pace or --pace-units explicitly enables throttling, also under --once;\n")
VLog.LogStr(" when absent the defaults apply (on for resident; off under --once). directory granularity is always 16.\n")
VLog.LogStr(" --first-no-pace round 1 scans without throttling (affects round 1 only; later rounds follow normal rules).\n")
VLog.LogStr(" differs from --pace 0: the latter disables throttling for the whole run. meaningful only for multi-round resident mode.\n")
VLog.LogStr(" --need-confirm <sec> heartbeat confirmation: an independent watchdog child asks for one stdin line every <sec> seconds\n")
VLog.LogStr(" {\"ok\": 1}; if no input arrives in time or ok!=1, prints {\"exit\":1, ...}\n")
VLog.LogStr(" and terminates this process (exit code 1). default/0 = off; scan mode only.\n")
VLog.LogStr(" --need-confirm-timeout <ms>\n")
VLog.LogStr(" confirmation wait timeout (default 5000; meaningful only when --need-confirm is given).\n")
VLog.LogStr(" --verbose print detailed process logs (progress/scan roots/per-item filtering/probe details);\n")
VLog.LogStr(" quiet by default: only the startup summary, heartbeat prompts, warnings and one round-end line.\n")
VLog.LogStr(" --path <dir> force-replace the common directory table (repeatable); once present no auto table is built.\n")
VLog.LogStr(" --search-paths print the paths to search then exit: build the common directory table and list it line by line\n")
VLog.LogStr(" (index + path + table source, to stderr); no scan,\n")
VLog.LogStr(" no watchdog, no cache access; --cache is optional in this case.\n")
# 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
Cache_Path = None
Once_Flag = 0
Need_Confirm = 0
Sleep_Ms = DEFAULT_SLEEP_MS
Need_Confirm_Timeout = DEFAULT_CONFIRM_TIMEOUT_MS
Verbose_Flag = 0
Search_Paths_Flag = 0
Pace_Set = 0
Pace_Ms = DEFAULT_PACE_SLEEP_MS
Pace_Units = DEFAULT_PACE_FILE_UNITS
First_No_Pace_Flag = 0
if Search_Paths_Implicit != 0:
# 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
while i < sys._argc:
A: str = sys._argv[i]
if A is not None:
if string.strcmp(A, "--cache") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
Cache_Path = sys._argv[i + 1]
i += 1
else:
VLog.LogStr("Error: --cache requires a path argument\n")
return 1
elif string.strcmp(A, "--once") == 0:
Once_Flag = 1
elif string.strcmp(A, "--verbose") == 0:
Verbose_Flag = 1 # valueless switch: changes stderr verbosity only
elif string.strcmp(A, "--search-paths") == 0:
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)
if M < 0:
VLog.LogFmt1("Error: --sleep requires an integer-literal millisecond value in 0..%u, got: ",
sizet(SLEEP_MAX_MS))
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
PrintUsage()
return 1
Sleep_Ms = M
i += 1
else:
VLog.LogStr("Error: --sleep requires a millisecond argument\n")
PrintUsage()
return 1
elif string.strcmp(A, "--pace") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
P: int = ParseUint(sys._argv[i + 1], PACE_SLEEP_MAX_MS)
if P < 0:
VLog.LogFmt1("Error: --pace requires an integer-literal millisecond value in 0..%u, got: ",
sizet(PACE_SLEEP_MAX_MS))
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
PrintUsage()
return 1
Pace_Ms = P
Pace_Set = 1 # explicitly given => throttling on (also under --once)
i += 1
else:
VLog.LogStr("Error: --pace requires a millisecond argument\n")
PrintUsage()
return 1
elif string.strcmp(A, "--pace-units") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
U: int = ParseUint(sys._argv[i + 1], PACE_UNITS_MAX)
# 0 and invalid are both errors; 0 makes "a sleep point every 0 files" meaningless
if U <= 0:
VLog.LogFmt1("Error: --pace-units requires an integer-literal file count in 1..%u, got: ",
sizet(PACE_UNITS_MAX))
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
PrintUsage()
return 1
Pace_Units = U
Pace_Set = 1 # explicitly given => throttling on (also under --once)
i += 1
else:
VLog.LogStr("Error: --pace-units requires a file-count argument\n")
PrintUsage()
return 1
elif string.strcmp(A, "--first-no-pace") == 0:
# 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:
# two fixed-arity calls on one line (LogFmt1 takes ints, LogFmtS1 takes strings)
VLog.LogFmt1("Error: --need-confirm requires an integer-literal second value in 0..%u, got: ",
sizet(SECS_MAX))
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
PrintUsage()
return 1
Need_Confirm = S
i += 1
else:
VLog.LogStr("Error: --need-confirm requires a second argument\n")
PrintUsage()
return 1
elif string.strcmp(A, "--need-confirm-timeout") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
T: int = ParseUint(sys._argv[i + 1], CONFIRM_TIMEOUT_MAX_MS)
if T < 0:
VLog.LogFmt1("Error: --need-confirm-timeout requires an integer-literal millisecond value in 0..%u, got: ",
sizet(CONFIRM_TIMEOUT_MAX_MS))
VLog.LogFmtS1("%s\n", sys._argv[i + 1])
PrintUsage()
return 1
Need_Confirm_Timeout = T
Has_Timeout = 1
i += 1
else:
VLog.LogStr("Error: --need-confirm-timeout requires a millisecond argument\n")
PrintUsage()
return 1
elif string.strcmp(A, "--path") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
VDirs.BeginOverride()
VDirs.AddOverridePath(sys._argv[i + 1])
i += 1
else:
VLog.LogStr("Error: --path requires a directory argument\n")
return 1
else:
VLog.LogFmtS1("Error: unknown argument %s\n", A)
PrintUsage()
return 1
i += 1
# --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("Error: scan mode requires --cache <path>\n")
PrintUsage()
return 1
# --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("Warning: --need-confirm-timeout is used without --need-confirm, ignored\n")
return 0
# confirm-watch (hidden internal subcommand) argument parsing: e.g. VET.exe confirm-watch
# --interval <s> --timeout <ms> --parent-pid <pid> --ipc-write <handle> [--verbose]; not in usage.
def ParseWatchArgs() -> int:
global Watch_Interval, Watch_Timeout, Watch_ParentPid, Watch_IpcWrite, Watch_Verbose
Watch_Interval = 0
Watch_Timeout = -1
Watch_ParentPid = 0
Watch_IpcWrite = 0
Watch_Verbose = 0
Has_Pid: int = 0
i: int = 2
while i < sys._argc:
A: str = sys._argv[i]
if A is not None:
if string.strcmp(A, "--interval") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
S: int = ParseUint(sys._argv[i + 1], SECS_MAX)
if S <= 0:
VLog.LogStr("confirm-watch: --interval requires an integer-literal second value >= 1\n")
return 1
Watch_Interval = S
i += 1
else:
VLog.LogStr("confirm-watch: --interval requires a second argument\n")
return 1
elif string.strcmp(A, "--timeout") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
T: int = ParseUint(sys._argv[i + 1], CONFIRM_TIMEOUT_MAX_MS)
if T < 0:
VLog.LogStr("confirm-watch: --timeout requires a non-negative integer-literal millisecond value\n")
return 1
Watch_Timeout = T
i += 1
else:
VLog.LogStr("confirm-watch: --timeout requires a millisecond argument\n")
return 1
elif string.strcmp(A, "--parent-pid") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
P: longlong = ParseUint(sys._argv[i + 1], PID_MAX)
if P <= 0:
VLog.LogStr("confirm-watch: --parent-pid requires a positive integer\n")
return 1
Watch_ParentPid = P
Has_Pid = 1
i += 1
else:
VLog.LogStr("confirm-watch: --parent-pid requires a pid argument\n")
return 1
elif string.strcmp(A, "--ipc-write") == 0:
if i + 1 < sys._argc and sys._argv[i + 1] is not None:
if ParseUint64(sys._argv[i + 1]) == 0:
VLog.LogFmtS1("confirm-watch: --ipc-write requires a decimal non-negative integer-literal handle value, got: ",
sys._argv[i + 1])
VLog.LogStr("\n")
return 1
Watch_IpcWrite = U64_Val
i += 1
else:
VLog.LogStr("confirm-watch: --ipc-write requires a handle-value argument\n")
return 1
elif string.strcmp(A, "--verbose") == 0:
Watch_Verbose = 1 # valueless switch: appended by the parent when it is verbose
else:
VLog.LogFmtS1("confirm-watch: unknown argument %s\n", A)
return 1
i += 1
if Watch_Interval <= 0:
VLog.LogStr("confirm-watch: missing --interval <sec>\n")
return 1
if Has_Pid == 0:
VLog.LogStr("confirm-watch: missing --parent-pid <pid>\n")
return 1
if Watch_Timeout < 0:
Watch_Timeout = DEFAULT_CONFIRM_TIMEOUT_MS
return 0