206 lines
7.2 KiB
Python
206 lines
7.2 KiB
Python
#!/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)
|