mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-09 22:13:43 +08:00
1ce9f73ae2
Add navigation and map matching providers with the vendored map service sources.
215 lines
8.4 KiB
Python
215 lines
8.4 KiB
Python
"""Fixed, opt-in manager ownership for the diagnostic Mapd shadow process."""
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import time
|
|
|
|
from opendbc.car.structs import car
|
|
from openpilot.common.basedir import BASEDIR
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.swaglog import cloudlog
|
|
from openpilot.system.manager.process import NativeProcess
|
|
from openpilot.starpilot.maps.artifact import elf_arm64_static_file, source_digest
|
|
from openpilot.starpilot.maps.storage import STORAGE_ANCHOR, offline_root as map_offline_root
|
|
|
|
|
|
PROVIDER_DIR = Path(BASEDIR) / 'openpilot/starpilot/maps/provider'
|
|
PROVIDER_BINARY = PROVIDER_DIR / 'mapd'
|
|
PROVIDER_MANIFEST = PROVIDER_DIR / 'manifest.json'
|
|
OFFLINE_ROOT = map_offline_root()
|
|
OPT_IN_KEY = 'MapdShadowEnabled'
|
|
MAX_MANIFEST_BYTES = 4096
|
|
MAX_SELECTOR_BYTES = 256
|
|
MAX_RECEIPT_BYTES = 16 << 20
|
|
|
|
|
|
def _open_regular(path: Path, *, max_bytes: int | None = None, executable: bool = False):
|
|
descriptor = os.open(path, os.O_RDONLY | os.O_NONBLOCK | getattr(os, 'O_NOFOLLOW', 0))
|
|
try:
|
|
info = os.fstat(descriptor)
|
|
if not stat.S_ISREG(info.st_mode) or (max_bytes is not None and info.st_size > max_bytes) or (executable and not info.st_mode & 0o111):
|
|
raise ValueError('not an acceptable regular file')
|
|
return os.fdopen(descriptor, 'rb')
|
|
except Exception:
|
|
os.close(descriptor)
|
|
raise
|
|
|
|
|
|
def _owned_package(binary: Path, manifest: Path, package_dir: Path, checkout_root: Path) -> bool:
|
|
if binary != package_dir / 'mapd' or manifest != package_dir / 'manifest.json':
|
|
return False
|
|
try:
|
|
relative = package_dir.relative_to(checkout_root)
|
|
except ValueError:
|
|
return False
|
|
current = checkout_root
|
|
for part in ('.', *relative.parts):
|
|
if part != '.':
|
|
current = current / part
|
|
try:
|
|
if not stat.S_ISDIR(current.lstat().st_mode):
|
|
return False
|
|
except OSError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _owned_root(root: Path, anchor: Path) -> bool:
|
|
if root != map_offline_root(anchor):
|
|
return False
|
|
for path in (anchor, root.parent.parent, root.parent, root):
|
|
try:
|
|
if not stat.S_ISDIR(path.lstat().st_mode):
|
|
return False
|
|
except OSError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _unique_object(pairs):
|
|
result = {}
|
|
for name, value in pairs:
|
|
if name in result:
|
|
raise ValueError('duplicate snapshot JSON field')
|
|
result[name] = value
|
|
return result
|
|
|
|
|
|
def _invalid_json_constant(_value):
|
|
raise ValueError('nonfinite snapshot JSON number')
|
|
|
|
|
|
def _selected_snapshot(root: Path) -> bool:
|
|
"""Check bounded receipt identity; Go checks each requested tile lazily."""
|
|
try:
|
|
with _open_regular(root / 'current.json', max_bytes=MAX_SELECTOR_BYTES) as file:
|
|
selector = json.loads(file.read(MAX_SELECTOR_BYTES + 1), object_pairs_hook=_unique_object,
|
|
parse_constant=_invalid_json_constant)
|
|
if (type(selector) is not dict or set(selector) != {'version', 'generation'} or
|
|
type(selector['version']) is not int or selector['version'] != 1 or
|
|
type(selector['generation']) is not str or re.fullmatch(r'[0-9a-f]{64}', selector['generation']) is None):
|
|
return False
|
|
generations = root / 'generations'
|
|
generation = generations / selector['generation']
|
|
if not stat.S_ISDIR(generations.lstat().st_mode) or not stat.S_ISDIR(generation.lstat().st_mode):
|
|
return False
|
|
with _open_regular(generation / 'receipt.json', max_bytes=MAX_RECEIPT_BYTES) as file:
|
|
raw = file.read(MAX_RECEIPT_BYTES + 1)
|
|
if hashlib.sha256(raw).hexdigest() != selector['generation']:
|
|
return False
|
|
receipt = json.loads(raw, object_pairs_hook=_unique_object, parse_constant=_invalid_json_constant)
|
|
if (type(receipt) is not dict or receipt.get('version') != 1 or type(receipt.get('version')) is not int or
|
|
receipt.get('tileSchema') != 'offline.capnp:0xda3a0d9284ca402f' or
|
|
receipt.get('attribution') != '© OpenStreetMap contributors; https://www.openstreetmap.org/copyright (ODbL)' or
|
|
receipt.get('sourceVerified') is not False or
|
|
receipt.get('sourceKind') not in ('local-pbf', 'provided-group-archives') or
|
|
type(receipt.get('tiles')) is not list or not 0 < len(receipt['tiles']) <= 65536 or
|
|
type(receipt.get('inputs')) is not list or not receipt['inputs']):
|
|
return False
|
|
return True
|
|
except (OSError, ValueError, TypeError, OverflowError, RecursionError):
|
|
return False
|
|
|
|
|
|
def preflight(binary: Path = PROVIDER_BINARY, manifest_path: Path = PROVIDER_MANIFEST,
|
|
offline_root: Path = OFFLINE_ROOT, source: Path | None = None,
|
|
storage_anchor: Path = STORAGE_ANCHOR, package_dir: Path = PROVIDER_DIR,
|
|
checkout_root: Path = Path(BASEDIR)) -> bool:
|
|
"""Verify the fixed packaged artifact and an existing, non-legacy tile root."""
|
|
source = source or Path(BASEDIR) / 'mapd_repo'
|
|
try:
|
|
if (not _owned_root(offline_root, storage_anchor) or not _selected_snapshot(offline_root) or
|
|
not _owned_package(binary, manifest_path, package_dir, checkout_root)):
|
|
return False
|
|
with _open_regular(manifest_path, max_bytes=MAX_MANIFEST_BYTES) as file:
|
|
manifest = json.loads(file.read(MAX_MANIFEST_BYTES + 1))
|
|
if (not isinstance(manifest, dict) or type(manifest.get('schemaVersion')) is not int or manifest['schemaVersion'] != 1 or
|
|
manifest.get('goVersion') != 'go1.25.1' or manifest.get('target') != 'linux-arm64-static'):
|
|
return False
|
|
for name, pattern in (('sourceRevision', r'[0-9a-f]{40}'), ('upstreamRevision', r'[0-9a-f]{40}'),
|
|
('sourceDigest', r'[0-9a-f]{64}'), ('binarySha256', r'[0-9a-f]{64}')):
|
|
value = manifest.get(name)
|
|
if type(value) is not str or re.fullmatch(pattern, value) is None:
|
|
return False
|
|
if manifest.get('sourceDigest') != source_digest(source):
|
|
return False
|
|
pinned = json.loads((Path(BASEDIR) / 'upstream-sync.json').read_text())
|
|
revision = next(item['commit'] for item in pinned['dependencies'] if item['path'] == 'mapd_repo')
|
|
if manifest.get('upstreamRevision') != revision:
|
|
return False
|
|
digest = hashlib.sha256()
|
|
stamps = {manifest['sourceDigest'].encode(), revision.encode(), manifest['sourceRevision'].encode()}
|
|
seen = set()
|
|
carry = b''
|
|
with _open_regular(binary, executable=True) as file:
|
|
if not elf_arm64_static_file(file):
|
|
return False
|
|
file.seek(0)
|
|
for chunk in iter(lambda: file.read(1024 * 1024), b''):
|
|
digest.update(chunk)
|
|
window = carry + chunk
|
|
seen.update(stamp for stamp in stamps if stamp in window)
|
|
carry = window[-128:]
|
|
return digest.hexdigest() == manifest.get('binarySha256') and stamps == seen
|
|
except (OSError, ValueError, KeyError, StopIteration, TypeError, OverflowError, RecursionError):
|
|
return False
|
|
|
|
|
|
def enabled(started: bool, params: Params, CP: car.CarParams) -> bool:
|
|
if not started or CP.notCar:
|
|
return False
|
|
try:
|
|
with _open_regular(Path(params.get_param_path(OPT_IN_KEY)), max_bytes=1) as file:
|
|
return file.read(2) == b'1'
|
|
except (OSError, ValueError):
|
|
return False
|
|
|
|
|
|
class MapdShadowProcess(NativeProcess):
|
|
"""NativeProcess with bounded retry for an exited or unavailable shadow."""
|
|
|
|
def __init__(self, clock=time.monotonic):
|
|
super().__init__('mapd_shadow', str(PROVIDER_DIR.relative_to(BASEDIR)),
|
|
[str(PROVIDER_BINARY), '--shadow', '--offline-root', str(OFFLINE_ROOT)], enabled)
|
|
self._clock = clock
|
|
self._failures = 0
|
|
self._retry_at = 0.0
|
|
self._started_at = 0.0
|
|
|
|
def _delay_retry(self, now: float) -> None:
|
|
self._failures += 1
|
|
self._retry_at = now + min(2 ** min(self._failures - 1, 6), 60)
|
|
|
|
def start(self) -> None:
|
|
now = self._clock()
|
|
if self.shutting_down:
|
|
NativeProcess.stop(self)
|
|
if self.proc is not None:
|
|
return
|
|
if self.proc is not None:
|
|
if self.proc.exitcode is None:
|
|
if now - self._started_at >= 60:
|
|
self._failures = 0
|
|
return
|
|
NativeProcess.stop(self, block=False) # reap a failed direct child
|
|
self._delay_retry(now)
|
|
return
|
|
if now < self._retry_at:
|
|
return
|
|
if not preflight():
|
|
cloudlog.warning('mapd shadow package or offline root unavailable')
|
|
self._delay_retry(now)
|
|
return
|
|
NativeProcess.start(self)
|
|
self._started_at = now
|
|
|
|
def stop(self, retry: bool = True, block: bool = True, sig=None):
|
|
result = NativeProcess.stop(self, retry=retry, block=block, sig=sig)
|
|
self._failures = 0
|
|
self._retry_at = 0.0
|
|
return result
|