mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-25 13:19:03 -05:00
Two test modules still unpacked nine values from verify_pack_against_platform. The category evidence joined readers with semicolons, which the prose rule refuses on generated pages, and three comments used clause semicolons.
162 lines
6.8 KiB
Python
162 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Turn install.py --events into tab-separated records for the launcher.
|
|
|
|
The launcher is bash and draws through PortMaster's dialog FIFO, so it reads
|
|
one record per line with `IFS=$'\\t' read`. Text fields are cleaned for that
|
|
FIFO: PortMasterDialog sends its arguments through an unquoted
|
|
`echo $(printf ...)`, where a tab or newline splits a field and `*`, `?` or
|
|
`[` expand as a file pattern.
|
|
|
|
rbevents.py status EVENTS records of a --check run
|
|
rbevents.py follow SUMMARY an install run on stdin: progress
|
|
records on stdout, the outcome in SUMMARY
|
|
|
|
Python 3.8 and the standard library, like install.py.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Iterable, Iterator, TextIO
|
|
|
|
_UNSAFE = re.compile(r"[\x00-\x1f\x7f*?\[\]\\]+")
|
|
_SPACES = re.compile(r" {2,}")
|
|
# A system id becomes a bash array subscript and a menu key in the launcher.
|
|
# Every manifest uses slugs, and anything else is dropped, never cleaned into one.
|
|
_SYSTEM_ID = re.compile(r"[A-Za-z0-9._-]+")
|
|
|
|
|
|
def clean(value: object) -> str:
|
|
"""One FIFO-safe field: no control character, pattern character or run of spaces."""
|
|
text = _UNSAFE.sub(" ", str(value))
|
|
return _SPACES.sub(" ", text).strip()
|
|
|
|
|
|
def size_text(count: int) -> str:
|
|
"""12 KB, 3.4 MB, 1.2 GB."""
|
|
value = float(count)
|
|
for unit in ("B", "KB", "MB", "GB"):
|
|
if value < 1024 or unit == "GB":
|
|
return f"{value:.0f} {unit}" if unit in ("B", "KB") else f"{value:.1f} {unit}"
|
|
value /= 1024
|
|
return f"{value:.1f} GB"
|
|
|
|
|
|
def events(lines: Iterable[str]) -> Iterator[dict]:
|
|
"""The JSON objects of an event stream. A line that is not one is skipped."""
|
|
for line in lines:
|
|
line = line.strip()
|
|
if not line.startswith("{"):
|
|
continue
|
|
try:
|
|
event = json.loads(line)
|
|
except ValueError:
|
|
continue
|
|
if isinstance(event, dict) and isinstance(event.get("event"), str):
|
|
yield event
|
|
|
|
|
|
def record(out: TextIO, *fields: object) -> None:
|
|
# A tab is whitespace to `read`, so two in a row are one separator: an
|
|
# empty field would shift the ones after it. "-" stands for it.
|
|
out.write("\t".join(clean(field) or "-" for field in fields) + "\n")
|
|
out.flush()
|
|
|
|
|
|
def status(stream: Iterable[str], out: TextIO) -> None:
|
|
"""The records the start screen and the menus read.
|
|
|
|
Only the first detected platform is described: a handheld runs one.
|
|
"""
|
|
platform = None
|
|
ended = False
|
|
for event in events(stream):
|
|
kind = event["event"]
|
|
if kind == "start":
|
|
record(out, "start", event.get("installer", ""))
|
|
elif kind == "detect":
|
|
found = event.get("platforms") or []
|
|
record(out, "detect", event.get("os_name", ""), len(found))
|
|
if found:
|
|
first = found[0]
|
|
platform = first.get("platform")
|
|
record(out, "platform", platform, first.get("name", ""),
|
|
first.get("bios_dir", ""))
|
|
elif kind == "error":
|
|
record(out, "error", event.get("reason", ""), event.get("message", ""))
|
|
elif kind == "system" and event.get("platform") == platform:
|
|
if not _SYSTEM_ID.fullmatch(str(event.get("system", ""))):
|
|
continue
|
|
needed = event.get("missing", 0) + event.get("wrong_hash", 0)
|
|
system = event.get("system", "")
|
|
# install.py files what belongs to no single system under "other".
|
|
name = "Files of no single system" if system == "other" else event.get("name", "")
|
|
record(out, "system", system, name,
|
|
event.get("files", 0), event.get("present", 0), event.get("missing", 0),
|
|
event.get("wrong_hash", 0), size_text(event.get("bytes_needed", 0)),
|
|
"1" if needed else "0")
|
|
elif kind == "check" and event.get("platform") == platform:
|
|
record(out, "check", event.get("files", 0), event.get("present", 0),
|
|
event.get("missing", 0), event.get("wrong_hash", 0),
|
|
size_text(event.get("bytes_needed", 0)), event.get("data_missing", 0),
|
|
event.get("not_installable", 0))
|
|
elif kind == "end":
|
|
ended = True
|
|
record(out, "end", event.get("code", 1))
|
|
if not ended:
|
|
# Refused before the run started, or stopped without a word.
|
|
record(out, "end", "none")
|
|
|
|
|
|
def follow(stream: Iterable[str], out: TextIO, summary: TextIO) -> None:
|
|
"""Progress records as an install runs, and its outcome in *summary*."""
|
|
ended = False
|
|
for event in events(stream):
|
|
kind = event["event"]
|
|
if kind == "download":
|
|
record(out, "download", event.get("files", 0), size_text(event.get("bytes", 0)))
|
|
elif kind == "downloaded":
|
|
record(out, "file", event.get("files_done", 0), event.get("files_total", 0),
|
|
"ok" if event.get("ok") else "failed", event.get("dest", ""),
|
|
size_text(event.get("bytes_done", 0)),
|
|
size_text(event.get("bytes_total", 0)))
|
|
elif kind == "progress":
|
|
record(out, "progress", event.get("files_done", 0), event.get("files_total", 0),
|
|
size_text(event.get("bytes_done", 0)),
|
|
size_text(event.get("bytes_total", 0)))
|
|
elif kind == "error":
|
|
record(out, "error", event.get("reason", ""), event.get("message", ""))
|
|
record(summary, "error", event.get("reason", ""), event.get("message", ""))
|
|
elif kind == "summary":
|
|
record(summary, "summary", event.get("installed", 0), event.get("up_to_date", 0),
|
|
event.get("pending", 0), "1" if event.get("interrupted") else "0")
|
|
for failure in event.get("failed") or []:
|
|
record(summary, "failed", failure.get("dest", ""), failure.get("reason", ""))
|
|
for group in event.get("skipped") or []:
|
|
record(summary, "skipped", group.get("label", ""), group.get("files", 0),
|
|
group.get("required", 0))
|
|
elif kind == "end":
|
|
ended = True
|
|
record(summary, "end", event.get("code", 1))
|
|
if not ended:
|
|
record(summary, "end", "none")
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
if len(argv) == 2 and argv[0] == "status":
|
|
with open(argv[1], encoding="utf-8", errors="replace") as stream:
|
|
status(stream, sys.stdout)
|
|
return 0
|
|
if len(argv) == 2 and argv[0] == "follow":
|
|
with Path(argv[1]).open("w", encoding="utf-8") as summary:
|
|
follow(sys.stdin, sys.stdout, summary)
|
|
return 0
|
|
sys.stderr.write("usage: rbevents.py status EVENTS | follow SUMMARY\n")
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|