Add channel-aware release tooling and updater recovery
This commit is contained in:
223
ptlinux/window_diagnostics.py
Normal file
223
ptlinux/window_diagnostics.py
Normal file
@@ -0,0 +1,223 @@
|
||||
"""Bounded structured diagnostics for the two game-window supervisors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
WINDOW_DIAGNOSTICS_ENV = "PTLINUX_WINDOW_DIAGNOSTICS"
|
||||
WINDOW_DIAGNOSTICS_SESSION_ENV = "PTLINUX_WINDOW_DIAGNOSTICS_SESSION"
|
||||
WINDOW_DIAGNOSTIC_PREFIX = "PTLINUX_WINDOW_DIAGNOSTIC "
|
||||
WINDOW_DIAGNOSTIC_EVENT_LIMIT = 12
|
||||
WINDOW_DIAGNOSTIC_LINE_LIMIT = 4096
|
||||
WINDOW_DIAGNOSTIC_COMPONENTS = frozenset({"game_window", "gamescope_host"})
|
||||
_DIAGNOSTIC_NAME_PATTERN = re.compile(r"[a-z][a-z0-9_]{0,63}\Z")
|
||||
_SESSION_ID_PATTERN = re.compile(r"[0-9a-f]{32}\Z")
|
||||
_TRUE_VALUES = frozenset({"1", "true", "yes"})
|
||||
_MAX_DETAILS_DEPTH = 4
|
||||
_MAX_DETAILS_ITEMS = 48
|
||||
_MAX_STRING_CHARACTERS = 256
|
||||
|
||||
|
||||
def window_diagnostics_enabled(
|
||||
environment: Mapping[str, str] | None = None,
|
||||
) -> bool:
|
||||
env = os.environ if environment is None else environment
|
||||
return env.get(WINDOW_DIAGNOSTICS_ENV, "").strip().casefold() in _TRUE_VALUES
|
||||
|
||||
|
||||
class WindowDiagnosticRecorder:
|
||||
"""Emit a few samples while retaining aggregate counts for session shutdown."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
component: str,
|
||||
*,
|
||||
environment: Mapping[str, str] | None = None,
|
||||
monotonic: Callable[[], float] | None = None,
|
||||
) -> None:
|
||||
if component not in WINDOW_DIAGNOSTIC_COMPONENTS:
|
||||
raise ValueError(f"unsupported window diagnostic component: {component}")
|
||||
env = os.environ if environment is None else environment
|
||||
session_id = env.get(WINDOW_DIAGNOSTICS_SESSION_ENV, "").strip().casefold()
|
||||
self.component = component
|
||||
self.enabled = window_diagnostics_enabled(env)
|
||||
self.session_id = (
|
||||
session_id if _SESSION_ID_PATTERN.fullmatch(session_id) is not None else None
|
||||
)
|
||||
self._monotonic = monotonic or time.monotonic
|
||||
self._started_at = self._monotonic()
|
||||
self._counts: Counter[str] = Counter()
|
||||
self._transitions: dict[str, object] = {}
|
||||
self._sequence = 0
|
||||
self._emitted = 0
|
||||
self._closed = False
|
||||
|
||||
@property
|
||||
def accepts_samples(self) -> bool:
|
||||
return (
|
||||
self.enabled
|
||||
and not self._closed
|
||||
and self._emitted < WINDOW_DIAGNOSTIC_EVENT_LIMIT
|
||||
)
|
||||
|
||||
def record(self, diagnostic: str, **details: object) -> None:
|
||||
if self._closed or _DIAGNOSTIC_NAME_PATTERN.fullmatch(diagnostic) is None:
|
||||
return
|
||||
self._sequence += 1
|
||||
self._counts[diagnostic] += 1
|
||||
if not self.enabled or self._emitted >= WINDOW_DIAGNOSTIC_EVENT_LIMIT:
|
||||
return
|
||||
self._emitted += 1
|
||||
self._emit(diagnostic, self._sequence, details)
|
||||
|
||||
def transition(
|
||||
self,
|
||||
key: str,
|
||||
value: object,
|
||||
diagnostic: str,
|
||||
**details: object,
|
||||
) -> None:
|
||||
"""Record one diagnostic only when the observed state changes."""
|
||||
|
||||
if self._closed or self._transitions.get(key, object()) == value:
|
||||
return
|
||||
self._transitions[key] = value
|
||||
self.record(diagnostic, **details)
|
||||
|
||||
def close(self, **details: object) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
if not self.enabled:
|
||||
return
|
||||
summary = {
|
||||
**details,
|
||||
"counts": dict(sorted(self._counts.items())),
|
||||
"sampled_events": self._emitted,
|
||||
"suppressed_events": max(0, self._sequence - self._emitted),
|
||||
}
|
||||
self._emit("session_summary", self._sequence + 1, summary)
|
||||
|
||||
def _emit(
|
||||
self,
|
||||
diagnostic: str,
|
||||
sequence: int,
|
||||
details: Mapping[str, object],
|
||||
) -> None:
|
||||
payload: dict[str, object] = {
|
||||
"schema": 1,
|
||||
"component": self.component,
|
||||
"diagnostic": diagnostic,
|
||||
"sequence": sequence,
|
||||
"elapsed_ms": max(0, round((self._monotonic() - self._started_at) * 1000)),
|
||||
"details": _bounded_value(details, depth=0),
|
||||
}
|
||||
if self.session_id is not None:
|
||||
payload["session_id"] = self.session_id
|
||||
try:
|
||||
encoded = json.dumps(
|
||||
payload,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=True,
|
||||
)
|
||||
if len(encoded.encode("utf-8")) > WINDOW_DIAGNOSTIC_LINE_LIMIT:
|
||||
payload["details"] = {"payload_truncated": True}
|
||||
encoded = json.dumps(
|
||||
payload,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=True,
|
||||
)
|
||||
print(
|
||||
f"{WINDOW_DIAGNOSTIC_PREFIX}{encoded}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
except (OSError, TypeError, ValueError):
|
||||
return
|
||||
|
||||
|
||||
def parse_window_diagnostic(line: str) -> dict[str, object] | None:
|
||||
"""Parse only launcher-generated, bounded window diagnostic records."""
|
||||
|
||||
marker_index = line.find(WINDOW_DIAGNOSTIC_PREFIX)
|
||||
if marker_index < 0:
|
||||
return None
|
||||
encoded = line[marker_index + len(WINDOW_DIAGNOSTIC_PREFIX):].strip()
|
||||
if not encoded or len(encoded.encode("utf-8")) > WINDOW_DIAGNOSTIC_LINE_LIMIT:
|
||||
return None
|
||||
try:
|
||||
payload = json.loads(encoded)
|
||||
except (json.JSONDecodeError, UnicodeError):
|
||||
return None
|
||||
if not isinstance(payload, dict) or payload.get("schema") != 1:
|
||||
return None
|
||||
component = payload.get("component")
|
||||
diagnostic = payload.get("diagnostic")
|
||||
sequence = payload.get("sequence")
|
||||
elapsed_ms = payload.get("elapsed_ms")
|
||||
details = payload.get("details")
|
||||
session_id = payload.get("session_id")
|
||||
if (
|
||||
not isinstance(component, str)
|
||||
or component not in WINDOW_DIAGNOSTIC_COMPONENTS
|
||||
or not isinstance(diagnostic, str)
|
||||
or _DIAGNOSTIC_NAME_PATTERN.fullmatch(diagnostic) is None
|
||||
or not isinstance(sequence, int)
|
||||
or isinstance(sequence, bool)
|
||||
or sequence < 1
|
||||
or not isinstance(elapsed_ms, int)
|
||||
or isinstance(elapsed_ms, bool)
|
||||
or elapsed_ms < 0
|
||||
or not isinstance(details, dict)
|
||||
or (
|
||||
session_id is not None
|
||||
and (
|
||||
not isinstance(session_id, str)
|
||||
or _SESSION_ID_PATTERN.fullmatch(session_id) is None
|
||||
)
|
||||
)
|
||||
):
|
||||
return None
|
||||
return {
|
||||
"schema": 1,
|
||||
"component": component,
|
||||
"diagnostic": diagnostic,
|
||||
"sequence": sequence,
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"details": _bounded_value(details, depth=0),
|
||||
**({"session_id": session_id} if isinstance(session_id, str) else {}),
|
||||
}
|
||||
|
||||
|
||||
def _bounded_value(value: object, *, depth: int) -> object:
|
||||
if depth >= _MAX_DETAILS_DEPTH:
|
||||
return "<max-depth>"
|
||||
if value is None or isinstance(value, (bool, int)):
|
||||
return value
|
||||
if isinstance(value, float):
|
||||
return round(value, 3) if math.isfinite(value) else None
|
||||
if isinstance(value, str):
|
||||
return value[:_MAX_STRING_CHARACTERS]
|
||||
if isinstance(value, Mapping):
|
||||
result: dict[str, object] = {}
|
||||
for index, (raw_key, item) in enumerate(value.items()):
|
||||
if index >= _MAX_DETAILS_ITEMS:
|
||||
result["_truncated"] = True
|
||||
break
|
||||
key = str(raw_key)[:64]
|
||||
result[key] = _bounded_value(item, depth=depth + 1)
|
||||
return result
|
||||
if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray)):
|
||||
items = list(value[:_MAX_DETAILS_ITEMS])
|
||||
return [_bounded_value(item, depth=depth + 1) for item in items]
|
||||
return f"<{type(value).__name__}>"
|
||||
Reference in New Issue
Block a user