514 lines
22 KiB
Plaintext
514 lines
22 KiB
Plaintext
from stdint import *
|
|
import string
|
|
import viperlib
|
|
import memhub
|
|
import lib.VFs as VFs
|
|
import lib.VLog as VLog
|
|
import lib.VPe as VPe
|
|
import lib.VGuid as VGuid
|
|
import lib.VJson as VJson
|
|
import lib.VCache as VCache
|
|
import lib.VCheck as VCheck
|
|
import lib.VDirs as VDirs
|
|
import lib.VProc as VProc
|
|
import lib.VGuard as VGuard
|
|
import lib.VCli as VCli
|
|
import win32.win32file as wf
|
|
import win32.win32base as wb
|
|
|
|
|
|
# VScan - scan subcommand: directory tree walk, candidate filtering, progress, round driver.
|
|
# Contract: stdout = one full JSON array per round; stderr = logs (quiet by default, --verbose details).
|
|
|
|
define MAX_SCAN_DEPTH = 6 # max recursion depth
|
|
define PACE_DIR_UNITS = 16 # sleep every 16 directory entries
|
|
define PACE_FILE_UNITS = 64 # sleep every 64 files
|
|
define PACE_SLEEP_MS = 20 # Sleep(20) at each throttle point
|
|
define PROG_INTERVAL_MS = 1000 # min interval between progress lines
|
|
define PROG_STAMP_CAP = 64 # timestamp text buffer
|
|
define PROG_ELAPSED_CAP = 32 # "3.2s" elapsed text buffer
|
|
define REJECT_LOG_MAX = 20 # max per-line [reject] logs; rest summarized at round end
|
|
# WIN32_FIND_DATAA: cFileName starts at offset 44
|
|
define FD_ATTRIBUTES_OFF = 0
|
|
define FD_FILENAME_OFF = 44
|
|
define FIND_DATA_SIZE = 320 # sizeof(WIN32_FIND_DATAA): 318 aligned to 4 = 320
|
|
# winnt.h: reparse points (symlinks/mounts); skipped in recursion to avoid cycles
|
|
define FILE_ATTRIBUTE_REPARSE_POINT = 0x400
|
|
|
|
# injected by App/main.vp
|
|
_mbuddy: memhub.MemBuddy*
|
|
|
|
# scan static buffers (reused for process lifetime; single-threaded)
|
|
Scr_Ver: bytes = None
|
|
Scr_Arch: bytes = None
|
|
Scr_Id: bytes = None
|
|
Scr_Disp: bytes = None
|
|
Probe_Buf: bytes = None
|
|
Prog_Stamp: bytes = None
|
|
Prog_Elapsed: bytes = None
|
|
|
|
# throttle sleep switch: 1 = on (resident default), 0 = off (RunScan sets 0 for --once)
|
|
Pace_Enabled: int = 1
|
|
|
|
# Throttle switch for a regular round (computed by RunScan); Pace_Enabled is this round's value.
|
|
# RunScanRound restores it each round, then applies --first-no-pace to round 1 only.
|
|
Pace_Configured: int = 1
|
|
|
|
# Throttle duration and file granularity (mutable); defaults below unless --pace/--pace-units given.
|
|
# RunScan overrides them when given; then --once no longer disables throttling.
|
|
Pace_Sleep_Ms: int = PACE_SLEEP_MS # --pace <ms>; 0 = no Sleep at either throttle point
|
|
Pace_File_Units: int = PACE_FILE_UNITS # --pace-units <N>; file-side granularity (dir side stays 16)
|
|
|
|
# throttle counters
|
|
Pace_Dir_Count: int = 0
|
|
Pace_File_Count: int = 0
|
|
|
|
# progress state
|
|
Prog_Round: int = 0
|
|
Prog_Roots: int = 0
|
|
Prog_T0: ULONGLONG = 0
|
|
Prog_LastTick: ULONGLONG = 0
|
|
Prog_Dirs: int = 0
|
|
Prog_Files: int = 0
|
|
Prog_Cands: int = 0
|
|
Prog_Cur: str = None
|
|
Round_Added: int = 0
|
|
|
|
# [reject] failure-path counters (reset each round; single-threaded)
|
|
Reject_Shown: int = 0 # [reject] lines printed this round (cap REJECT_LOG_MAX)
|
|
Reject_Total: int = 0 # total candidates filtered by PE/.PRVERSN this round
|
|
|
|
|
|
# ---- Scan strategy helpers ----
|
|
|
|
# whether to skip this directory name (blacklist, case-insensitive)
|
|
def IsSkippedDir(Name: str) -> int:
|
|
if VFs.StrEqNoCase(Name, "WinSxS") != 0:
|
|
return 1
|
|
if VFs.StrEqNoCase(Name, "$Recycle.Bin") != 0:
|
|
return 1
|
|
if VFs.StrEqNoCase(Name, "System Volume Information") != 0:
|
|
return 1
|
|
if VFs.StrEqNoCase(Name, "node_modules") != 0:
|
|
return 1
|
|
if VFs.StrEqNoCase(Name, ".git") != 0:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
# whether the file name ends with .exe (case-insensitive)
|
|
def HasExeSuffix(Name: str) -> int:
|
|
n: sizet = string.strlen(Name)
|
|
if n < 4:
|
|
return 0
|
|
a: int = int(Name[int(n) - 4])
|
|
b: int = int(Name[int(n) - 3])
|
|
d0: int = int(Name[int(n) - 2])
|
|
e0: int = int(Name[int(n) - 1])
|
|
if a >= 65 and a <= 90:
|
|
a += 32
|
|
if b >= 65 and b <= 90:
|
|
b += 32
|
|
if d0 >= 65 and d0 <= 90:
|
|
d0 += 32
|
|
if e0 >= 65 and e0 <= 90:
|
|
e0 += 32
|
|
if a == 46 and b == 101 and d0 == 120 and e0 == 101: # ".exe"
|
|
return 1
|
|
return 0
|
|
|
|
|
|
# Progress output (all via VLog -> stderr; quiet unless --verbose).
|
|
# Counter and throttle-baseline updates are unconditional; only the print step uses Verbose*.
|
|
|
|
# print one progress line (r/dir/file/cand/cur/elapsed) and refresh the time baseline
|
|
def ProgPrint():
|
|
global Prog_LastTick
|
|
Now: ULONGLONG = wb.GetTickCount64()
|
|
Prog_LastTick = Now
|
|
ElapsedMs: sizet = sizet(Now - Prog_T0)
|
|
Secs: sizet = ElapsedMs // 1000
|
|
Tenths: sizet = (ElapsedMs % 1000) // 100
|
|
viperlib.snprintf(Prog_Elapsed, sizet(PROG_ELAPSED_CAP), "%u.%us",
|
|
uint32(Secs), uint32(Tenths))
|
|
Cur: str = Prog_Cur
|
|
if Cur is None:
|
|
Cur = "?"
|
|
VLog.VerboseFmt4("[scan] r=%u dir=%u file=%u cand=%u",
|
|
sizet(Prog_Round), sizet(Prog_Dirs), sizet(Prog_Files), sizet(Prog_Cands))
|
|
VLog.VerboseFmtS2(" cur=%s elapsed=%s\n", Cur, str(Prog_Elapsed))
|
|
|
|
|
|
# print only if >= PROG_INTERVAL_MS since the last progress output
|
|
def ProgMaybePrint():
|
|
Now: ULONGLONG = wb.GetTickCount64()
|
|
if Now - Prog_LastTick >= ULONGLONG(PROG_INTERVAL_MS):
|
|
ProgPrint()
|
|
|
|
|
|
# round start: reset counters and print (round / root count / timestamp)
|
|
def ProgReset(RoundIdx: int, Roots: int):
|
|
global Prog_Round, Prog_Roots, Prog_T0, Prog_LastTick
|
|
global Prog_Dirs, Prog_Files, Prog_Cands, Prog_Cur
|
|
Prog_Round = RoundIdx
|
|
Prog_Roots = Roots
|
|
Prog_T0 = wb.GetTickCount64()
|
|
Prog_LastTick = Prog_T0
|
|
Prog_Dirs = 0
|
|
Prog_Files = 0
|
|
Prog_Cands = 0
|
|
Prog_Cur = None
|
|
VLog.TimeStampNow(Prog_Stamp, PROG_STAMP_CAP)
|
|
VLog.VerboseFmt2("==== scan round %u start (roots=%u", sizet(RoundIdx), sizet(Roots))
|
|
VLog.VerboseFmtS1(", t=%s) ====\n", str(Prog_Stamp))
|
|
|
|
|
|
def ProgRootEnter(Dir: str):
|
|
VLog.VerboseFmtS1(" [root] enter: %s\n", Dir)
|
|
|
|
|
|
def ProgRootLeave(Dir: str, ElapsedMs: sizet):
|
|
global Prog_Cur
|
|
VLog.VerboseFmtIS(" [root] leave (%u ms elapsed): %s\n", ElapsedMs, Dir)
|
|
Prog_Cur = None # path buffer is freed after this root; avoid a dangling pointer
|
|
|
|
|
|
def PaceDir():
|
|
global Pace_Dir_Count, Prog_Dirs
|
|
VGuard.PollIpc() # check-channel poll point (non-blocking; returns if idle)
|
|
Pace_Dir_Count += 1
|
|
Prog_Dirs += 1
|
|
if Pace_Dir_Count >= PACE_DIR_UNITS: # dir granularity is always 16 (--pace-units affects file side only)
|
|
Pace_Dir_Count = 0
|
|
# Pace_Sleep_Ms == 0 (--pace 0): do not call Sleep(0) -- it yields the time slice, not
|
|
# "disable throttle sleep". Counters and progress output still advance.
|
|
if Pace_Enabled != 0 and Pace_Sleep_Ms > 0:
|
|
wb.Sleep(ULONG(Pace_Sleep_Ms))
|
|
ProgPrint()
|
|
else:
|
|
ProgMaybePrint()
|
|
|
|
|
|
def PaceFile():
|
|
global Pace_File_Count, Prog_Files
|
|
VGuard.PollIpc() # check-channel poll point (non-blocking; returns if idle)
|
|
Pace_File_Count += 1
|
|
Prog_Files += 1
|
|
if Pace_File_Count >= Pace_File_Units: # file granularity set by --pace-units (default 64)
|
|
Pace_File_Count = 0
|
|
if Pace_Enabled != 0 and Pace_Sleep_Ms > 0: # same rationale as PaceDir (--pace 0 no Sleep(0))
|
|
wb.Sleep(ULONG(Pace_Sleep_Ms))
|
|
ProgMaybePrint()
|
|
|
|
|
|
# ---- Init ----
|
|
|
|
# allocate scan static buffers (reused for process lifetime)
|
|
def ScanInit() -> int:
|
|
global Scr_Ver, Scr_Arch, Scr_Id, Scr_Disp, Probe_Buf, Prog_Stamp, Prog_Elapsed
|
|
Scr_Ver = _mbuddy.alloc(VPe.VER_CAP)
|
|
Scr_Arch = _mbuddy.alloc(VPe.ARCH_CAP)
|
|
Scr_Id = _mbuddy.alloc(VGuid.GUID_STR_CAP)
|
|
Scr_Disp = _mbuddy.alloc(VCache.DISP_CAP)
|
|
Probe_Buf = _mbuddy.alloc(VProc.VERSION_PROBE_DIAG_CAP)
|
|
Prog_Stamp = _mbuddy.alloc(PROG_STAMP_CAP)
|
|
Prog_Elapsed = _mbuddy.alloc(PROG_ELAPSED_CAP)
|
|
if Scr_Ver is None or Scr_Arch is None or Scr_Id is None or Scr_Disp is None \
|
|
or Probe_Buf is None or Prog_Stamp is None or Prog_Elapsed is None:
|
|
VLog.LogStr("[VET] FATAL: scan buffer allocation failed\n")
|
|
return 0
|
|
if VJson.JsonInit() == 0:
|
|
VLog.LogStr("[VET] FATAL: scan buffer allocation failed\n")
|
|
return 0
|
|
# check channel buffers/reply pool: on failure degrade only (check replies
|
|
# CHANNEL_UNAVAILABLE, no revalidation removal); scan itself is unaffected.
|
|
if VCheck.CheckInit() == 0:
|
|
VLog.LogStr("Warning: check channel buffers unavailable; check requests only get the failure reason (CHANNEL_UNAVAILABLE), discovered entries are not revalidated each round\n")
|
|
return 1
|
|
|
|
|
|
# ---- Candidate handling ----
|
|
|
|
# --version prefix probe (5s timeout; timeout/startup failure => fail, not cached)
|
|
def ProbeVersion(Target: str) -> int:
|
|
rc: int = VProc.RunVersionProbe(Target, Probe_Buf, sizet(VProc.VERSION_PROBE_DIAG_CAP), 1)
|
|
if rc == 0:
|
|
if VProc.OutputHasViperPrefix(str(Probe_Buf)) != 0:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
# Handle one .exe candidate: PE + .PRVERSN filter, then --version check; on hit add to cache.
|
|
# Return 1 = new hit, 0 = miss/existing/failure (TryReadPeInfo rc codes: see VPe.vp).
|
|
def HandleCandidate(ExeAnsi: str) -> int:
|
|
global Prog_Cands, Round_Added, Reject_Shown, Reject_Total
|
|
if VCache.CacheFind(ExeAnsi) >= 0:
|
|
return 0 # known: do not call --version again
|
|
rc: int = VPe.TryReadPeInfo(ExeAnsi, Scr_Ver, VPe.VER_CAP, Scr_Arch, VPe.ARCH_CAP)
|
|
if rc != 0:
|
|
# not PE or no .PRVERSN: log first REJECT_LOG_MAX lines, rest summarized at round end (--verbose)
|
|
Reject_Total += 1
|
|
if Reject_Shown < REJECT_LOG_MAX:
|
|
Reject_Shown += 1
|
|
VLog.VerboseFmtIS(" [reject] rc=%u %s\n", sizet(rc), ExeAnsi)
|
|
return 0
|
|
Prog_Cands += 1
|
|
# Call --version only for exes passing the PE + .PRVERSN filter (few).
|
|
# Poll the check channel before probing (a probe can take 5s; check outranks scanning).
|
|
VGuard.PollIpc()
|
|
if ProbeVersion(ExeAnsi) == 0:
|
|
VLog.VerboseFmtS1(" [skip] PE+PRVERSN hit but --version prefix mismatch: %s\n", ExeAnsi)
|
|
return 0
|
|
if VGuid.NewGuidStr(Scr_Id, VGuid.GUID_STR_CAP) == 0:
|
|
VLog.LogStr(" Warning: CoCreateGuid failed, skipping this entry\n")
|
|
return 0
|
|
viperlib.snprintf(Scr_Disp, sizet(VCache.DISP_CAP), "Viper %s", Scr_Ver)
|
|
if VCache.CacheAdd(ExeAnsi, str(Scr_Id), str(Scr_Ver), str(Scr_Arch),
|
|
"workspace", str(Scr_Disp)) == 0:
|
|
return 0
|
|
Round_Added += 1
|
|
VLog.VerboseFmtS2(" [hit] %s (version %s)\n", ExeAnsi, str(Scr_Ver))
|
|
return 1
|
|
|
|
|
|
# ---- Directory tree walk ----
|
|
|
|
# Recursively enumerate a directory for .exe files; Depth starts at 1, capped by MAX_SCAN_DEPTH.
|
|
# Two passes (dir\* then dir\*.exe) share one buffer; HasExeSuffix guards 8.3 short-name false hits.
|
|
def ScanTree(DirAnsi: str, Depth: int) -> int:
|
|
global Prog_Cur
|
|
Prog_Cur = DirAnsi # current directory for the progress line
|
|
Pat: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
|
Fd: bytes = _mbuddy.alloc(FIND_DATA_SIZE)
|
|
if Pat is None or Fd is None:
|
|
if Pat is not None:
|
|
_mbuddy.free(rawptr(Pat))
|
|
if Fd is not None:
|
|
_mbuddy.free(rawptr(Fd))
|
|
return 0
|
|
|
|
# ---- (1) dirs: enumerate subdirectories and recurse ----
|
|
if VFs.JoinPath(Pat, VFs.PATH_BUF_CAP, DirAnsi, "*") == 0:
|
|
_mbuddy.free(rawptr(Pat))
|
|
_mbuddy.free(rawptr(Fd))
|
|
return 0
|
|
hDir: wb.HANDLE = wf.FindFirstFileA(Pat, rawptr(Fd))
|
|
if hDir != wb.INVALID_HANDLE_VALUE:
|
|
while True:
|
|
PaceDir()
|
|
Attrs: uint32 = VPe.ReadU32(BYTEPTR(Fd), FD_ATTRIBUTES_OFF)
|
|
Name: str = str(Fd + FD_FILENAME_OFF)
|
|
if string.strcmp(Name, ".") != 0 and string.strcmp(Name, "..") != 0:
|
|
if (Attrs & wf.FILE_ATTRIBUTE_DIRECTORY) != 0:
|
|
# skip reparse points (symlinks/mounts) and blacklisted dirs to avoid cycles/noise
|
|
if (Attrs & FILE_ATTRIBUTE_REPARSE_POINT) == 0 and IsSkippedDir(Name) == 0:
|
|
if Depth < MAX_SCAN_DEPTH:
|
|
Child: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
|
if Child is not None:
|
|
if VFs.JoinPath(Child, VFs.PATH_BUF_CAP, DirAnsi, Name) != 0:
|
|
ScanTree(str(Child), Depth + 1)
|
|
Prog_Cur = DirAnsi # restore after recursion (Child is about to be freed)
|
|
_mbuddy.free(rawptr(Child))
|
|
if wf.FindNextFileA(hDir, rawptr(Fd)) == 0:
|
|
break
|
|
wf.FindClose(hDir)
|
|
|
|
# ---- (2) files: enumerate only *.exe (such dirs were recursed in (1), skip here) ----
|
|
if VFs.JoinPath(Pat, VFs.PATH_BUF_CAP, DirAnsi, "*.exe") != 0:
|
|
hExe: wb.HANDLE = wf.FindFirstFileA(Pat, rawptr(Fd))
|
|
if hExe != wb.INVALID_HANDLE_VALUE:
|
|
while True:
|
|
Attrs2: uint32 = VPe.ReadU32(BYTEPTR(Fd), FD_ATTRIBUTES_OFF)
|
|
Name2: str = str(Fd + FD_FILENAME_OFF)
|
|
if (Attrs2 & wf.FILE_ATTRIBUTE_DIRECTORY) == 0:
|
|
PaceFile()
|
|
if HasExeSuffix(Name2) != 0:
|
|
Full: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
|
if Full is not None:
|
|
if VFs.JoinPath(Full, VFs.PATH_BUF_CAP, DirAnsi, Name2) != 0:
|
|
HandleCandidate(str(Full))
|
|
_mbuddy.free(rawptr(Full))
|
|
if wf.FindNextFileA(hExe, rawptr(Fd)) == 0:
|
|
break
|
|
wf.FindClose(hExe)
|
|
|
|
_mbuddy.free(rawptr(Pat))
|
|
_mbuddy.free(rawptr(Fd))
|
|
return 0
|
|
|
|
|
|
# ---- One scan round ----
|
|
|
|
# Fully revalidate discovered entries and remove failures; returns the removed count.
|
|
# Same rule as VCheck.VerifyExecutable; runs before tree scan, polls check before each probe (<=5s).
|
|
def RevalidateDiscovered() -> int:
|
|
removed: int = 0
|
|
j: int = 0
|
|
while j < VCache.CacheCount():
|
|
Exe: str = VCache.CacheExe(j) # fetch first (RemoveAt frees this slot)
|
|
VGuard.PollIpc() # poll before probe (check first)
|
|
if VCheck.VerifyExecutable(Exe) == 0:
|
|
VLog.VerboseFmtS1(" [revalidate-remove] %s\n", Exe)
|
|
VCache.RemoveAt(j)
|
|
removed += 1
|
|
else:
|
|
j += 1
|
|
return removed
|
|
|
|
|
|
# Run one scan round: poll check -> reload cache.json -> revalidate -> walk dirs (polling check)
|
|
# -> validate existence -> poll check -> emit JSON to stdout -> write cache file.
|
|
def RunScanRound(CachePath: str, RoundIdx: int) -> int:
|
|
global Round_Added, Reject_Shown, Reject_Total
|
|
global Pace_Enabled
|
|
Round_Added = 0
|
|
Reject_Shown = 0
|
|
Reject_Total = 0
|
|
# This round's throttle switch: restore the regular value (RunScan computes it) then apply
|
|
# --first-no-pace for round 1 only, so the special case never leaks into later rounds.
|
|
Pace_Enabled = Pace_Configured
|
|
if VCli.FirstNoPace() != 0 and RoundIdx == 1:
|
|
Pace_Enabled = 0
|
|
T0: ULONGLONG = wb.GetTickCount64()
|
|
ProgReset(RoundIdx, VDirs.DirCount())
|
|
# Round start (order matters): reload cache.json, then handle queued check requests
|
|
# (after reload so new hits survive), then revalidate discovered entries before tree scan.
|
|
VCache.ReloadCache(CachePath)
|
|
VGuard.PollIpc()
|
|
Revalidated: int = RevalidateDiscovered()
|
|
Dirs: int = 0
|
|
for k in range(VDirs.DirCount()):
|
|
AnsiDir: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
|
if AnsiDir is not None:
|
|
VFs.Utf8ToAnsi(VDirs.DirAt(k), AnsiDir, VFs.PATH_BUF_CAP)
|
|
if VFs.PathIsDir(str(AnsiDir)) != 0:
|
|
Dirs += 1
|
|
R0: ULONGLONG = wb.GetTickCount64()
|
|
ProgRootEnter(str(AnsiDir))
|
|
ScanTree(str(AnsiDir), 1)
|
|
ProgRootLeave(str(AnsiDir), sizet(wb.GetTickCount64() - R0))
|
|
else:
|
|
# Static auto-table entries and drive letters may not exist; logging each is
|
|
# process detail, given only with --verbose (explicit --path warnings are in VDirs).
|
|
VLog.VerboseFmtS1(" skipped (missing or not a directory): %s\n", str(AnsiDir))
|
|
_mbuddy.free(rawptr(AnsiDir))
|
|
|
|
# Candidates filtered by PE/.PRVERSN: first REJECT_LOG_MAX were logged in HandleCandidate;
|
|
# only a summary line is added here (both are process detail, --verbose only).
|
|
if Reject_Total > REJECT_LOG_MAX:
|
|
VLog.VerboseFmt2(" [reject] rest omitted... (round total %u, first %u printed individually)\n",
|
|
sizet(Reject_Total), sizet(REJECT_LOG_MAX))
|
|
|
|
# Final poll before round end: no wait point before writing, so check hits enter this round's array
|
|
VGuard.PollIpc()
|
|
removed: int = VCache.ValidateCache()
|
|
VJson.EmitJson()
|
|
VLog.StdoutWrite(str(VJson.JsonBuf()))
|
|
VCache.WriteCacheFile(CachePath, VJson.JsonBuf(), sizet(VJson.JsonLen()))
|
|
|
|
T1: ULONGLONG = wb.GetTickCount64()
|
|
# Round-end line (kept by default, no paths): liveness signal, or the final line under --once.
|
|
# "Removed" = revalidation removals (Revalidated) + existence-check removals (removed).
|
|
VLog.LogFmt3("round %u end: scanned dirs %u, known entries %u",
|
|
sizet(RoundIdx), sizet(Dirs), sizet(VCache.CacheCount()))
|
|
VLog.LogFmt3(", added %u, removed %u, elapsed %u ms",
|
|
sizet(Round_Added), sizet(Revalidated + removed), sizet(T1 - T0))
|
|
# next-round-start timestamp is process detail (--verbose only)
|
|
if VLog.VerboseOn() != 0:
|
|
VLog.TimeStampAfter(Prog_Stamp, PROG_STAMP_CAP, sizet(VCli.SleepMs()))
|
|
VLog.LogFmtS1(", next round starts: %s", str(Prog_Stamp))
|
|
VLog.LogStr("\n")
|
|
return 0
|
|
|
|
|
|
# scan subcommand: args already parsed and watchdog started by App/main.vp (VCli.ParseScanArgs).
|
|
# This function only initializes and runs the resident loop (round interval --sleep, 10000; 0 = no sleep).
|
|
def RunScan() -> int:
|
|
global Pace_Enabled, Pace_Configured, Pace_Sleep_Ms, Pace_File_Units
|
|
CachePath: str = VCli.CachePath()
|
|
Once: int = VCli.Once()
|
|
|
|
# Startup summary (kept, one line): tool name + version + mode + param summary, no paths --
|
|
# it is only a "process started" signal, forming the liveness indicator with the round-end line.
|
|
VLog.LogFmtS1("VET - Viper Environment Tool %s (scan)", VCli.Version())
|
|
VLog.LogFmt2(" sleep=%u once=%u", sizet(VCli.SleepMs()), sizet(Once))
|
|
VLog.LogFmt1(" need-confirm=%u\n", sizet(VCli.NeedConfirm()))
|
|
|
|
if VCli.PaceSet() != 0:
|
|
# Explicit --pace or --pace-units => always enable throttling (even with --once), using
|
|
# command-line values; this branch precedes the --once check, so --once keeps it on.
|
|
Pace_Enabled = 1
|
|
Pace_Configured = 1
|
|
Pace_Sleep_Ms = VCli.PaceSleepMs()
|
|
Pace_File_Units = VCli.PaceFileUnits()
|
|
VLog.VerboseFmt2("--pace: scan throttling enabled sleep=%u units=%u\n",
|
|
sizet(Pace_Sleep_Ms), sizet(Pace_File_Units))
|
|
elif Once != 0:
|
|
# No explicit throttle args: --once runs one round with throttle sleep disabled
|
|
Pace_Enabled = 0
|
|
Pace_Configured = 0
|
|
VLog.VerboseStr("--once: scan throttling sleep disabled\n")
|
|
# --first-no-pace: affects round 1's actual switch only (RunScanRound restores + applies it);
|
|
# so do not set Pace_Enabled here, just log one line (process detail, --verbose only).
|
|
if VCli.FirstNoPace() != 0:
|
|
VLog.VerboseStr("--first-no-pace: round 1 runs without throttle sleep\n")
|
|
|
|
if ScanInit() == 0:
|
|
return 1
|
|
|
|
VLog.VerboseStr("[0/3] common directory table\n")
|
|
rc0: int = VDirs.BuildTable()
|
|
if rc0 == 0:
|
|
VLog.VerboseStr(" ✓ pass\n\n")
|
|
else:
|
|
# Empty table = no scan roots this round: a functional anomaly, still reported by default
|
|
VLog.LogStr("Warning: common directory table is empty (no scan roots this round)\n\n")
|
|
|
|
# Cache load moved to each round start (RunScanRound -> VCache.ReloadCache); no separate
|
|
# read here, avoiding double parsing in round 1 (--once also goes through round 1).
|
|
|
|
Round: int = 0
|
|
while True:
|
|
Round += 1
|
|
RunScanRound(CachePath, Round)
|
|
if Once != 0:
|
|
break
|
|
# --sleep: round interval (default 10000; 0 = no sleep, start next round immediately)
|
|
SleepMs: int = VCli.SleepMs()
|
|
if SleepMs > 0:
|
|
wb.Sleep(ULONG(SleepMs))
|
|
return 0
|
|
|
|
|
|
# scan query switch --search-paths: build the searchable dir table, print it line by line to
|
|
# stderr, return 0. Reuses VDirs.BuildTable; no scanning, no cache, exit code always 0.
|
|
def RunSearchPathsQuery() -> int:
|
|
VDirs.BuildTable()
|
|
Src: str = "auto table"
|
|
if VDirs.IsOverride() != 0:
|
|
Src = "override table (--path)"
|
|
VLog.LogFmtS1("[search] directory table source: %s\n", Src)
|
|
N: int = VDirs.DirCount()
|
|
if N <= 0:
|
|
# empty table must be stated explicitly so the user does not think it hung
|
|
VLog.LogStr("[search] no searchable paths (common directory table is empty)\n")
|
|
return 0
|
|
Ansi: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
|
if Ansi is None:
|
|
VLog.LogStr(" Warning: query buffer allocation failed, listing paths only (no mark for scan-skip)\n")
|
|
for i in range(N):
|
|
Path: str = VDirs.DirAt(i)
|
|
Skip: int = 0 # 1 = will be skipped when scanning; stays 0 if Ansi unavailable
|
|
if Ansi is not None:
|
|
VFs.Utf8ToAnsi(Path, Ansi, VFs.PATH_BUF_CAP)
|
|
if VFs.PathIsDir(str(Ansi)) == 0:
|
|
Skip = 1
|
|
if Skip != 0:
|
|
VLog.LogFmtIS(" [search] [%u] %s (missing or not a directory, skipped when scanning)\n", sizet(i), Path)
|
|
else:
|
|
VLog.LogFmtIS(" [search] [%u] %s\n", sizet(i), Path)
|
|
if Ansi is not None:
|
|
_mbuddy.free(rawptr(Ansi))
|
|
VLog.LogFmt1(" [search] total %u; no scan, no cache access, query ends here\n", sizet(N))
|
|
return 0
|