#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Host-only RFFDG descriptor ownership and absolute deadline model.""" from __future__ import annotations from dataclasses import dataclass from phase10aq_worker_result_record import ( RECORD_SIZE, WorkerPrecommit, WorkerRecordError, parse_record, ) MAX_TICKS = 256 MAX_READ_EVENTS = 256 CREATE_PIPE = "CREATE_PIPE" SET_PARENT_READ_NONBLOCK = "SET_PARENT_READ_NONBLOCK" SPAWN_RFFDG = "SPAWN_RFFDG" PARENT_CLOSE_WRITE = "PARENT_CLOSE_WRITE" CHILD_CLOSE_READ = "CHILD_CLOSE_READ" CHILD_CLOSE_WRITE = "CHILD_CLOSE_WRITE" PARENT_CLOSE_READ = "PARENT_CLOSE_READ" TERMINATE_CHILD = "TERMINATE_CHILD" REAP_CHILD = "REAP_CHILD" OK = "OK" ERROR = "ERROR" DATA = "DATA" EINTR = "EINTR" WOULD_BLOCK = "WOULD_BLOCK" EOF = "EOF" OPERATIONS = { CREATE_PIPE, SET_PARENT_READ_NONBLOCK, SPAWN_RFFDG, PARENT_CLOSE_WRITE, CHILD_CLOSE_READ, CHILD_CLOSE_WRITE, PARENT_CLOSE_READ, TERMINATE_CHILD, REAP_CHILD, } READ_KINDS = {DATA, EINTR, WOULD_BLOCK, EOF} class FdModelError(RuntimeError): """The synthetic descriptor transaction cannot close safely.""" @dataclass(frozen=True) class FakeFdEvent: operation: str result: str ticks: int = 1 def __post_init__(self) -> None: if self.operation not in OPERATIONS or self.result not in {OK, ERROR}: raise FdModelError("FD event is invalid") if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \ or not 1 <= self.ticks <= MAX_TICKS: raise FdModelError("FD event ticks are invalid") @dataclass(frozen=True) class FakeRead: kind: str data: bytes = b"" ticks: int = 1 def __post_init__(self) -> None: if self.kind not in READ_KINDS or type(self.data) is not bytes: raise FdModelError("read event is invalid") if (self.kind == DATA) != bool(self.data) or len(self.data) > RECORD_SIZE: raise FdModelError("read payload is invalid") if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \ or not 1 <= self.ticks <= MAX_TICKS: raise FdModelError("read ticks are invalid") class FakeFdFacade: """Exact scripted FD facade with no OS, process, clock or I/O imports.""" def __init__(self, events: tuple[FakeFdEvent, ...]) -> None: if not isinstance(events, tuple) or not events \ or any(type(item) is not FakeFdEvent for item in events): raise FdModelError("FD script is invalid") self._events = list(events) self.ticks = 0 def invoke(self, operation: str, cleanup: bool = False) -> str: if not self._events: raise FdModelError("FD script is exhausted") event = self._events.pop(0) if event.operation != operation: raise FdModelError("FD operation ordering differs") if not cleanup and self.ticks + event.ticks > MAX_TICKS: raise FdModelError("absolute deadline precedes FD operation") self.ticks += event.ticks return event.result @property def remaining(self) -> int: return len(self._events) @dataclass(frozen=True) class FdOutcome: classification: str success: bool buffered: int eintr_count: int parent_fds_open: int child_fds_open: int worker_alive: bool containment_required: bool rffdg_used: bool = True live_fd_present: bool = False def _close(facade: FakeFdFacade, operation: str) -> None: if facade.invoke(operation, cleanup=True) != OK: raise FdModelError(f"terminal cleanup failed: {operation}") def run_fd_transaction(precommit: WorkerPrecommit, facade: FakeFdFacade, reads: tuple[FakeRead, ...]) -> FdOutcome: """Model exact descriptor ownership and one absolute deadline.""" if type(precommit) is not WorkerPrecommit or type(facade) is not FakeFdFacade \ or not isinstance(reads, tuple) or not 1 <= len(reads) <= MAX_READ_EVENTS \ or any(type(item) is not FakeRead for item in reads): raise FdModelError("FD model boundary is invalid") parent_read = parent_write = child_read = child_write = worker = False failed = False buffer = bytearray() eintr_count = 0 try: if facade.invoke(CREATE_PIPE) != OK: failed = True else: parent_read = parent_write = True if facade.invoke(SET_PARENT_READ_NONBLOCK) != OK: failed = True elif facade.invoke(SPAWN_RFFDG) != OK: failed = True else: worker = child_read = child_write = True if facade.invoke(PARENT_CLOSE_WRITE) != OK: failed = True else: parent_write = False if facade.invoke(CHILD_CLOSE_READ) != OK: failed = True else: child_read = False for index, read in enumerate(reads): if facade.ticks + read.ticks > MAX_TICKS: failed = True break facade.ticks += read.ticks if read.kind == EINTR: eintr_count += 1 continue if read.kind == WOULD_BLOCK: continue if read.kind == EOF: failed = True break if len(buffer) + len(read.data) > RECORD_SIZE: failed = True break buffer.extend(read.data) if len(buffer) == RECORD_SIZE: if index != len(reads) - 1: failed = True break try: parse_record(bytes(buffer), precommit) except WorkerRecordError: failed = True break if len(buffer) != RECORD_SIZE: failed = True except FdModelError: failed = True if worker: if failed: _close(facade, TERMINATE_CHILD) if child_read: _close(facade, CHILD_CLOSE_READ) child_read = False if child_write: _close(facade, CHILD_CLOSE_WRITE) child_write = False _close(facade, REAP_CHILD) worker = False if parent_write: _close(facade, PARENT_CLOSE_WRITE) parent_write = False if parent_read: _close(facade, PARENT_CLOSE_READ) parent_read = False if facade.remaining: raise FdModelError("FD script has unused operations") if failed: return FdOutcome("OFFLINE_FD_TRANSACTION_CONTAINED", False, len(buffer), eintr_count, 0, 0, False, True) return FdOutcome("OFFLINE_FD_TRANSACTION_COMPLETE", True, RECORD_SIZE, eintr_count, 0, 0, False, False)