mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
978105ad9b
Document StarPilot setup and third-party attribution. Keep cloud authentication scoped to the selected provider. Co-authored-by: Daniel Koepping <elkoled@gmail.com> Co-authored-by: Danny <dannyirwin31@yahoo.com> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
145 lines
6.9 KiB
Python
145 lines
6.9 KiB
Python
"""Explicit offline normalization of caches from this exact compiled schema."""
|
|
|
|
from datetime import UTC, datetime
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import struct
|
|
|
|
import capnp
|
|
|
|
from openpilot.starpilot import schema_cache as cache
|
|
from openpilot.starpilot.state_migration import (
|
|
_atomic_write, _outside_params, _params_lock, _read_value, _save_snapshot, canonical_json, load_snapshot,
|
|
)
|
|
|
|
|
|
EVENT_KEYS = tuple(sorted(key for key, contract in cache.CONTRACTS.items() if contract.service is not None))
|
|
|
|
|
|
class NormalizationError(RuntimeError):
|
|
"""No incompatible cache is eligible; an I/O failure may leave a reported partial batch."""
|
|
|
|
|
|
class _EnvelopeSink:
|
|
def put(self, key, value, *, block):
|
|
self.raw = value
|
|
|
|
|
|
def _header(raw):
|
|
start = len(cache.MAGIC) + 4
|
|
size = struct.unpack_from('>I', raw, len(cache.MAGIC))[0]
|
|
return json.loads(raw[start:start + size])
|
|
|
|
|
|
def _values(value):
|
|
if isinstance(value, float):
|
|
return struct.pack('>d', value) # Preserve signed zero and nonfinite values during comparison.
|
|
if isinstance(value, dict):
|
|
return {key: _values(item) for key, item in value.items()}
|
|
if isinstance(value, list):
|
|
return [_values(item) for item in value]
|
|
return value
|
|
|
|
|
|
def _prepare(key, raw):
|
|
inspected = cache.inspect_cache(key, raw)
|
|
if inspected.status != 'valid' or inspected.payload is None:
|
|
raise NormalizationError(f'{key}: {inspected.status}: {inspected.reason}')
|
|
contract = cache.CONTRACTS[key]
|
|
if contract.service is None:
|
|
raise NormalizationError(f'{key}: only Event caches can be normalized')
|
|
try:
|
|
with contract.root.from_bytes(inspected.payload, traversal_limit_in_words=cache.MAX_PAYLOAD_BYTES // 8) as message:
|
|
# Event.to_dict suppresses union-member decode errors; decode the selected
|
|
# service explicitly before constructing a fresh typed message.
|
|
service_values = getattr(message, contract.service).to_dict()
|
|
decoded = message.to_dict()
|
|
if contract.service not in decoded or _values(decoded[contract.service]) != _values(service_values):
|
|
raise NormalizationError(f'{key}: selected service did not decode completely')
|
|
original_values = _values(decoded)
|
|
if _header(raw)['version'] == 2:
|
|
return raw
|
|
sink = _EnvelopeSink()
|
|
cache.put_cache(sink, key, contract.root.new_message(**decoded), block=True)
|
|
converted = cache.inspect_cache(key, sink.raw)
|
|
if converted.status != 'valid' or converted.payload is None or _header(sink.raw)['version'] != 2:
|
|
raise NormalizationError(f'{key}: current writer did not produce a valid v2 envelope')
|
|
with contract.root.from_bytes(converted.payload, traversal_limit_in_words=cache.MAX_PAYLOAD_BYTES // 8) as message:
|
|
if _values(message.to_dict()) != original_values:
|
|
raise NormalizationError(f'{key}: typed values changed during normalization')
|
|
except (ValueError, TypeError, capnp.KjException) as error:
|
|
raise NormalizationError(f'{key}: typed cache validation failed: {type(error).__name__}') from error
|
|
return sink.raw
|
|
|
|
|
|
def _read(path):
|
|
try:
|
|
return _read_value(path)
|
|
except FileNotFoundError:
|
|
return None
|
|
|
|
|
|
def _identity(raw):
|
|
return None if raw is None else {'sha256': hashlib.sha256(raw).hexdigest(), 'size': len(raw),
|
|
'version': _header(raw)['version'], 'schema_sha256': _header(raw)['schema_sha256']}
|
|
|
|
|
|
def normalize_current_event_caches(params, backup_dir, *, producers_stopped: bool = False):
|
|
"""Validate every present Event cache before backing up and writing any cache.
|
|
|
|
Run with cache producers stopped, before changing compiled schemas. This cannot
|
|
convert an incompatible v1 cache. It never changes CarParams or read-side APIs.
|
|
The Params lock covers comparisons and atomic replacements; each prepared
|
|
envelope comes from put_cache with a fully decoded current typed message.
|
|
"""
|
|
if producers_stopped is not True:
|
|
raise NormalizationError('Stop cache producers before explicitly normalizing caches')
|
|
namespace = Path(params.get_param_path(EVENT_KEYS[0])).parent.absolute()
|
|
storage = Path(backup_dir).absolute()
|
|
_outside_params(namespace, storage)
|
|
with _params_lock(namespace):
|
|
actual = namespace.resolve(strict=True)
|
|
originals = {key: _read(actual / key) for key in EVENT_KEYS}
|
|
prepared = {key: _prepare(key, raw) for key, raw in originals.items() if raw is not None}
|
|
changed = [key for key, raw in prepared.items() if raw != originals[key]]
|
|
report = {
|
|
'format': 'starpilot-current-event-cache-normalization', 'version': 1,
|
|
'recorded_utc': datetime.now(UTC).isoformat(), 'namespace': str(namespace),
|
|
'codec': {'path': cache.__file__, 'sha256': hashlib.sha256(Path(cache.__file__).read_bytes()).hexdigest()},
|
|
'normalizer': {'path': __file__, 'sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()},
|
|
'status': 'unchanged', 'backup': None, 'attempted': [], 'written': [],
|
|
'caches': {key: {'before': _identity(raw), 'after': _identity(prepared.get(key)),
|
|
'action': 'normalize' if key in changed else 'missing' if raw is None else 'unchanged'}
|
|
for key, raw in originals.items()},
|
|
}
|
|
if not changed:
|
|
return report
|
|
backup = _save_snapshot({key: raw for key, raw in originals.items() if raw is not None}, storage)
|
|
if load_snapshot(backup) != {key: raw for key, raw in originals.items() if raw is not None}:
|
|
raise NormalizationError('Backup readback differs from original caches')
|
|
report.update(status='prepared', backup=str(backup))
|
|
receipt = backup / 'normalization.json'
|
|
_atomic_write(receipt, canonical_json(report))
|
|
try:
|
|
if namespace.resolve(strict=True) != actual or any(_read(actual / key) != raw for key, raw in originals.items()):
|
|
raise NormalizationError('Cache source changed after validation; no caches were written')
|
|
for key in changed:
|
|
if namespace.resolve(strict=True) != actual or _read(actual / key) != originals[key]:
|
|
raise NormalizationError(f'{key}: source changed before replacement')
|
|
report['attempted'].append(key)
|
|
_atomic_write(receipt, canonical_json(report))
|
|
_atomic_write(actual / key, prepared[key])
|
|
report['written'].append(key)
|
|
if _read(actual / key) != prepared[key] or cache.inspect_cache(key, prepared[key]).status != 'valid':
|
|
raise NormalizationError(f'{key}: exact cache readback failed')
|
|
if any(_read(actual / key) != prepared.get(key) for key in EVENT_KEYS):
|
|
raise NormalizationError('Final cache readback differs from the prepared batch')
|
|
report['status'] = 'normalized'
|
|
_atomic_write(receipt, canonical_json(report))
|
|
except Exception as error:
|
|
report.update(status='failed', error=f'{type(error).__name__}: {error}')
|
|
_atomic_write(receipt, canonical_json(report))
|
|
raise NormalizationError(f'Normalization stopped; originals and partial-write receipt: {backup}') from error
|
|
return report
|