#!/usr/bin/env python3 """Collect a canonical server-state snapshot from cycle evidence directories. The differential loop (live run vs emulated run, run N vs run N+1) needs one machine-readable view of the server state a cycle produced. Cycles already write that state as text: labelled field variables (``LABEL[offset]=0xVALUE``), labelled hexdump blocks (an ``INSTANCE[0000]: w w w w``-style row, or a header line naming the label followed by ``OFFSET: w w w w`` rows -- e.g. ``LOGINIT-raw`` / ``LOGRM-raw`` raw task-log dumps), and ``vgsp-nocat`` record lines carrying ``recType=`` / ``errorCode=``. This harness discovers those files under evidence/session directories, normalizes every dump to little-endian u32 words, and emits one JSON snapshot that two runs can be compared with ``diff``. Schemas ------- ``greeneye-server-snapshot/1``:: {"schema": "greeneye-server-snapshot/1", "source": "", "created": "", "chunks": [{"label": "INSTANCE", "base": "0x0", "words": ["0x00000000", ...]}], "records": [{"kind": "record", "raw": "...", "rec_type": 5, "error_code": 0x57f1344, "source_name": "ASSERT"}]} ``greeneye-server-snapshot-diff/1``:: {"schema": "greeneye-server-snapshot-diff/1", "a": {"source": ..., "created": ...}, "b": {...}, "chunks": [{"label": "INSTANCE", "status": "changed|added|removed|unchanged", "base": {"a": "0x0", "b": "0x0"}, "words": {"a": 1024, "b": 1024}, "added": [{"index": 8, "value": "0x00000000"}], "removed": [{"index": 8, "value": "0x00000001"}], "changed": [{"index": 8, "a": "0x...", "b": "0x..."}]}], "records": {"added": [...], "removed": [...]}, "summary": {"identical": false, "chunks_added": 0, ...}} Normalization rules (the consumer depends on them): * words are 8-hex-digit lowercase strings of little-endian u32 values; * a ``LABEL[offset]=0xVALUE`` value wider than 32 bits becomes two words (low word first); its offset orders the entries and selects ``base``, and the values are appended in offset order (the offset is a table index there, not a byte address); * hexdump rows are placed by byte offset (``(offset - base) // 4``) when 4-byte aligned, so a row missing in one run still aligns the rest; * ``base`` is the smallest offset seen for the chunk (``0x0`` when none); * chunks are sorted by label; a repeated label is merged only when its base and word list are byte-identical, otherwise it is kept as ``LABEL#2``, ``LABEL#3`` ... and the collision is reported in the run stats; * non-empty records are deduplicated by raw line and sorted by raw line; * ``created`` is the only variable field: pin it with ``--created`` or ``SOURCE_DATE_EPOCH`` for deterministic output. Honesty: this is an evidence normalizer, not a state oracle. It preserves what the cycle wrote -- offsets are honoured, identical duplicates are merged, differing duplicates are renamed, and dump-looking lines that fail to parse are counted (``unparsed``) instead of being silently dropped. It never executes anything, touches no GPU and reads only files given on the command line. """ from __future__ import annotations import argparse import fnmatch import json import os import re import sys import tempfile import time from typing import Any, Iterable TOOL_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if TOOL_ROOT not in sys.path: sys.path.insert(0, TOOL_ROOT) SNAPSHOT_FORMAT = "greeneye-server-snapshot/1" DIFF_FORMAT = "greeneye-server-snapshot-diff/1" DEMO_FORMAT = "greeneye-server-snapshot-demo/1" #: File-name globs discovered under each ``--dir`` unless extended with #: ``--include``. fnmatch's ``*`` matches leading dots, so the rolling host #: dmesg captures (``.dmesg-*.rolling``) are covered by ``*.rolling``. DEFAULT_INCLUDES = ("*.txt", "*.log", "*.rolling") #: ``INSTANCE[0000]: w w w w`` -- one labelled dump row inside an arbitrary #: prefix (dmesg timestamp, subsystem, case id). Offsets are hexadecimal. _INLINE_ROW_RE = re.compile( r"(?P