This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user