Files
StarPilot/openpilot/starpilot/drive_state/owner.py
T
firestar5683 751d2369c5 Drive Lifecycle
Track drive identity and evidence lifecycle so consumers reset state at the same drive boundaries.
2026-10-14 19:56:17 -05:00

158 lines
6.0 KiB
Python

"""One session-bound request owner shared by local and authenticated controls."""
from collections.abc import Callable
from contextlib import contextmanager
from dataclasses import dataclass
import fcntl
import os
from pathlib import Path
import re
import stat
import threading
import uuid
from openpilot.common.params import UnknownKeyName
from .resolver import Mode
SESSION_KEY = 'DriveStateSession'
REQUEST_KEY = 'DriveStateRequest'
_HEX = re.compile(r'[0-9a-f]{32}\Z')
class Rejected(ValueError):
pass
@dataclass(frozen=True)
class State:
mode: Mode = Mode.AUTO
revision: str | None = None
available: bool = False
def valid_session(value) -> bool:
return (type(value) is dict and set(value) == {'version', 'id', 'pid', 'birth', 'boot'} and
type(value['version']) is int and value['version'] == 1 and
isinstance(value['id'], str) and _HEX.fullmatch(value['id']) is not None and
isinstance(value['boot'], str) and re.fullmatch(r'[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}', value['boot']) is not None and
all(type(value[key]) is int and value[key] > 0 for key in ('pid', 'birth')))
def request_state(value, session) -> State:
if (type(value) is not dict or set(value) != {'version', 'session', 'revision', 'mode'} or
type(value['version']) is not int or value['version'] != 1 or value['session'] != session['id'] or
not isinstance(value['revision'], str) or _HEX.fullmatch(value['revision']) is None):
return State()
try:
return State(Mode(value['mode']), value['revision'], True)
except (TypeError, ValueError):
return State()
def process_identity(pid: int):
try:
raw = Path(f"/proc/{pid}/stat").read_bytes()
fields = raw[raw.rfind(b')') + 2:].split()
return int(fields[19]), fields[0]
except (OSError, IndexError, ValueError):
return None, None
def manager_alive(session) -> bool:
try:
if Path('/proc/sys/kernel/random/boot_id').read_text().strip() != session['boot']:
return False
except OSError:
return False
birth, state = process_identity(session['pid'])
return birth == session['birth'] and state not in (None, b'Z', b'X')
class DriveStateOwner:
def __init__(self, params, root: Path, *, alive: Callable[[dict], bool] = manager_alive):
self.params, self.root, self.alive = params, Path(root), alive
self._lock = threading.RLock()
@contextmanager
def _exclusive(self):
with self._lock:
self.root.mkdir(mode=0o700, parents=True, exist_ok=True)
info = self.root.lstat()
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o022:
raise Rejected('Drive state storage is unavailable')
fd = os.open(self.root / '.lock', os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077:
raise Rejected('Drive state storage is unavailable')
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise Rejected("Drive state changed; refresh and try again") from None
yield
finally:
os.close(fd)
def _read(self):
try:
session = self.params.get(SESSION_KEY)
if not valid_session(session) or not self.alive(session):
return None, State()
return session, request_state(self.params.get(REQUEST_KEY), session)
except (OSError, KeyError, TypeError, ValueError, UnknownKeyName):
return None, State()
def snapshot(self) -> State:
return self._read()[1]
def initialize(self, *, pid: int, birth: int, boot: str) -> State:
session = {'version': 1, 'id': uuid.uuid4().hex, 'pid': pid, 'birth': birth, 'boot': boot}
if not valid_session(session) or not self.alive(session):
raise Rejected('Manager session is unavailable')
with self._exclusive():
request = self._record(session, Mode.AUTO)
# Mismatched generations resolve Auto during either write or a crash.
self.params.put(REQUEST_KEY, request, block=True)
self.params.put(SESSION_KEY, session, block=True)
return request_state(request, session)
def initialize_manager(self) -> State:
pid = os.getpid()
boot = Path('/proc/sys/kernel/random/boot_id').read_text().strip()
return self.initialize(pid=pid, birth=process_identity(pid)[0], boot=boot)
@staticmethod
def _record(session, mode):
return {'version': 1, 'session': session['id'], 'revision': uuid.uuid4().hex, 'mode': mode.value}
def request(self, mode: str, *, expected_revision: str, authorized: Callable[[], bool],
override_allowed: Callable[[], bool]) -> State:
try:
desired = Mode(mode)
except (TypeError, ValueError):
raise Rejected('Choose Auto, Offroad or Onroad') from None
with self._exclusive():
session, current = self._read()
if not current.available or current.revision != expected_revision:
raise Rejected('Drive state changed; refresh and try again')
if not authorized():
raise Rejected('Drive state changes are unavailable')
if desired == Mode.OFFROAD and not override_allowed():
raise Rejected('Park the vehicle and disengage before forcing Offroad')
if desired == current.mode:
return current
# These callbacks recheck the authenticated caller and physical source,
# rather than relying on the state that produced the button.
if not authorized() or (desired == Mode.OFFROAD and not override_allowed()) or not self.alive(session):
raise Rejected('Drive state changes are unavailable')
observed_session, observed = self._read()
if observed_session != session or observed != current:
raise Rejected('Drive state changed; refresh and try again')
record = self._record(session, desired)
if not authorized() or (desired == Mode.OFFROAD and not override_allowed()):
raise Rejected('Drive state changes are unavailable')
self.params.put(REQUEST_KEY, record, block=True)
return request_state(record, session)