#!/usr/bin/env python3
"""Stream a complete depot inventory and atomically publish its pathname list.
Uses the p4 CLI's marshalled output; P4Python is not required. One query covers
files directly at each depot root, then one query per top-level directory.
The output retains the existing format: one depotFile pathname per line,
including deleted head revisions reported by `p4 files`.
"""
import argparse
import logging
import marshal
import os
from pathlib import Path
import stat
import subprocess
import sys
import tempfile
class InventoryError(RuntimeError):
def __init__(self, message, returncode=1):
super().__init__(message)
self.returncode = returncode if returncode > 0 else 1
def _text(value):
return value.decode("utf-8", "surrogateescape") if isinstance(value, bytes) else str(value)
def records(binary, args, allow_empty=False):
"""Yield stat records while distinguishing empty queries from failures."""
with tempfile.TemporaryFile() as diagnostic:
process = subprocess.Popen([binary, "-G", "-ztag", *args],
stdout=subprocess.PIPE, stderr=diagnostic)
errors = []
empty = False
try:
# peek distinguishes a clean EOF from a truncated marshalled record.
while process.stdout.peek(1):
try:
raw = marshal.load(process.stdout)
except (EOFError, ValueError, TypeError) as exc:
raise InventoryError("Invalid or truncated p4 output for " + args[0]) from exc
if not isinstance(raw, dict):
raise InventoryError("Expected p4 dictionary output for " + args[0])
rec = {_text(key): value for key, value in raw.items()}
code = _text(rec.get("code", ""))
if code == "stat":
yield rec
elif code == "error":
generic = int(rec.get("generic", 0))
severity = int(rec.get("severity", 3))
# Perforce EV_EMPTY (0x11) means the query had no matches.
# Permission, connection and resource-limit errors never qualify.
if allow_empty and generic == 17 and severity <= 2:
empty = True
else:
errors.append(_text(rec.get("data", "p4 query failed")).strip())
elif code in ("info", "text"):
detail = _text(rec.get("data", "")).strip()
if detail:
logging.info("%s", detail)
else:
raise InventoryError("Unexpected p4 record type: " + code)
returncode = process.wait()
diagnostic.seek(0)
stderr = diagnostic.read().decode("utf-8", "replace").strip()
if stderr:
logging.warning("%s", stderr)
if errors or (returncode != 0 and not (empty and returncode == 1)):
detail = "; ".join(errors) or stderr or "p4 query failed"
raise InventoryError(detail, returncode)
finally:
process.stdout.close()
if process.poll() is None:
process.terminate()
process.wait()
def _field(record, name):
if name not in record:
raise InventoryError("p4 inventory record is missing " + name)
value = _text(record[name])
if not value or "\n" in value or "\r" in value:
raise InventoryError("Inventory names must fit on one line")
return value
def create_inventory(binary, output):
output = Path(output)
temporary = None
count = 0
try:
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", errors="surrogateescape",
newline="\n", dir=str(output.parent),
prefix="." + output.name + ".", delete=False) as stream:
temporary = Path(stream.name)
for depot in records(binary, ["depots"], allow_empty=True):
depot_path = "//" + _field(depot, "name")
logging.info("Scanning %s", depot_path)
# '*' never crosses '/', so these cannot overlap child scans.
for rec in records(binary, ["files", depot_path + "/*"], allow_empty=True):
stream.write(_field(rec, "depotFile") + "\n")
count += 1
for directory in records(binary, ["dirs", "-D", depot_path + "/*"], allow_empty=True):
directory_path = _field(directory, "dir").rstrip("/")
for rec in records(binary, ["files", directory_path + "/..."], allow_empty=True):
stream.write(_field(rec, "depotFile") + "\n")
count += 1
stream.flush()
os.fsync(stream.fileno())
if output.exists():
temporary.chmod(stat.S_IMODE(output.stat().st_mode))
os.replace(temporary, output)
temporary = None
finally:
if temporary is not None:
temporary.unlink()
logging.info("Published %d paths to %s", count, output)
return count
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--p4", required=True, help="Configured SDP p4 executable")
parser.add_argument("--output", default="p4_file_list.txt")
args = parser.parse_args(argv)
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
try:
create_inventory(args.p4, args.output)
except InventoryError as exc:
logging.error("%s", exc)
return exc.returncode
except (OSError, ValueError) as exc:
logging.error("Unable to create file inventory: %s", exc)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #1 | 34004 | Russell C. Jackson (Rusty) |
Improve SDP backup/recovery, Maintenance, logging and cloud operation. Add owned checkpoint locks, recovery-aware retention, preserved database switches, bounded cleanup and coordinated AWS/Azure snapshots. Fix service-user password initialization, edge staging, standby promotion state parsing, proxy cache setup, typed depot verification and historical-server configuration/error handling. Remove unused OTP programs; keep Windows removed, server.locks cleanup and the journal-count condition unchanged. Add no global shell strict modes. Update operational documentation and add reproducible mkdirs.sh/mkdirs.cfg container assurance. Validate Ubuntu 26.04 and UBI 8.10/9.8/10.2 across both Python runtimes: 44 stages; 428 offline tests per runtime/OS, zero offline skips; 19 deployed-archive comparisons per combination. Three six-node topology runs pass all 30 recovery/failure scenarios. P4D 2023.1 through 2026.1 on UBI 8/9 pass all 14 cells and 98 scenarios. Static, documentation and provenance checks pass. Containers do not certify every fleet kernel, patch build or integration. Tested source manifest SHA-256: e1a23e17b8ced8c1148d9a250589977c6f6e98606bf2a81dc3ff5d7c909e3695 Deployed comparison archive SHA-256: 1bd23217356ee3808e040d39aea5be9519bda7c8f2b4924656a70aff8ad7293a |