3"""Own the bench flock without exposing its descriptor to Bash children."""
5from __future__
import annotations
21from collections.abc
import Callable
22from contextlib
import suppress
23from dataclasses
import dataclass
24from pathlib
import Path
28MAX_FIELDS_BYTES = 8192
30 'import sys\nsys.stdout.write("READY\\n")\nsys.stdout.flush()\n'
31 "raise SystemExit(0 if sys.stdin.buffer.read(1) == b'x' else 1)"
35@dataclass(frozen=True)
37 """One authenticated broker request parsed from the fixed CLI."""
47class BrokerError(ValueError):
48 """The lock request or its parent holder identity was unsafe."""
51def _start_ticks(pid: int) -> int:
52 """Read one Linux process start time without parsing its comm as fields."""
54 raw = Path(f
"/proc/{pid}/stat").read_text(encoding=
"ascii")
55 return int(raw[raw.rindex(
")") + 2 :].split()[19])
56 except (OSError, ValueError, IndexError)
as exc:
57 msg = f
"cannot authenticate process {pid}"
58 raise BrokerError(msg)
from exc
61def _fields(encoded: str) -> dict[str, str]:
62 """Decode the existing bounded key=value hold request."""
64 raw = base64.b64decode(encoded, validate=
True)
65 text = raw.decode(
"utf-8",
"strict")
66 except (binascii.Error, UnicodeError)
as exc:
67 msg =
"hold fields are not canonical base64 UTF-8"
68 raise BrokerError(msg)
from exc
69 if len(raw) > MAX_FIELDS_BYTES:
70 msg =
"hold fields exceed their size bound"
71 raise BrokerError(msg)
72 result: dict[str, str] = {}
73 for line
in text.splitlines():
74 key, separator, value = line.partition(
"=")
75 if not separator
or key
in result:
76 msg =
"hold fields are malformed or duplicated"
77 raise BrokerError(msg)
91 if not required <= result.keys()
or result[
"resource"] !=
"bench":
92 msg =
"hold fields omit the bench identity"
93 raise BrokerError(msg)
97def _close_error(descriptor: int, closer: Callable[[int],
None]) -> OSError |
None:
98 """Attempt one numeric close exactly once and return its error."""
101 except OSError
as error:
109 opener: Callable[[Path, int, int], int] = os.open,
110 fd_stat: Callable[[int], os.stat_result] = os.fstat,
111 path_stat: Callable[[Path], os.stat_result] |
None =
None,
112 closer: Callable[[int],
None] = os.close,
113) -> tuple[int, os.stat_result]:
114 """Open one stable non-linked regular lock inode with CLOEXEC."""
115 flags = os.O_RDWR | os.O_CLOEXEC | getattr(os,
"O_NOFOLLOW", 0)
116 descriptor: int |
None =
None
119 descriptor = opener(path, flags, 0o666)
120 except FileNotFoundError:
126 descriptor = opener(path, flags | os.O_CREAT | os.O_EXCL, 0o666)
127 except FileExistsError:
128 descriptor = opener(path, flags, 0o666)
129 observed = fd_stat(descriptor)
130 current = path.stat(follow_symlinks=
False)
if path_stat
is None else path_stat(path)
131 inheritable = os.get_inheritable(descriptor)
132 except OSError
as exc:
133 close_failure =
None if descriptor
is None else _close_error(descriptor, closer)
134 msg = f
"cannot open canonical lock: {exc}"
135 error = BrokerError(msg)
136 if close_failure
is not None:
137 error.add_note(f
"lock descriptor close also failed: {close_failure}")
140 not stat.S_ISREG(observed.st_mode)
141 or observed.st_nlink != 1
142 or (observed.st_dev, observed.st_ino) != (current.st_dev, current.st_ino)
145 close_failure = _close_error(descriptor, closer)
146 msg =
"canonical lock is linked, replaced, non-regular, or inheritable"
147 error = BrokerError(msg)
148 if close_failure
is not None:
149 error.add_note(f
"lock descriptor close also failed: {close_failure}")
151 return descriptor, observed
154def _take(descriptor: int, wait_s: int) ->
None:
155 """Take the flock within a monotonic bounded wait."""
156 deadline = time.monotonic() + wait_s
157 while not _try_take(descriptor):
158 if time.monotonic() >= deadline:
159 raise BlockingIOError
163def _try_take(descriptor: int) -> bool:
164 """Attempt one nonblocking flock acquisition."""
166 fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
167 except BlockingIOError:
172def _record(fields: dict[str, str], host_pid: int, host_ticks: int) -> dict[str, object]:
173 """Construct telemetry only after the broker owns the live kernel lock."""
174 now = int(time.time())
176 boot = Path(
"/proc/sys/kernel/random/boot_id").read_text(encoding=
"ascii").strip()
177 broker_ticks = _start_ticks(os.getpid())
178 budget = int(fields[
"max_hold_s"])
179 except (OSError, ValueError)
as exc:
180 msg =
"cannot construct the live holder record"
181 raise BrokerError(msg)
from exc
184 "lock_id": fields[
"lock_id"],
185 "holder_class": fields[
"holder_class"],
186 "holder_name": fields[
"holder_name"],
188 "pid_start_ticks": broker_ticks,
189 "host_pid": host_pid,
190 "host_start_ticks": host_ticks,
192 "origin": fields[
"origin"],
193 "intent": fields[
"intent"],
194 "git_ref": fields[
"git_ref"],
195 "acquired_at": time.strftime(
"%Y-%m-%dT%H:%M:%S%z"),
196 "acquired_epoch": now,
197 "max_hold_s": budget,
198 "hold_kind": fields[
"hold_kind"],
199 "last_activity": time.strftime(
"%Y-%m-%dT%H:%M:%S%z"),
200 "break_glass": fields[
"break_glass"] ==
"true",
204def _publish(path: Path, record: dict[str, object]) -> os.stat_result:
205 """Atomically publish and durably bind one holder-record inode."""
206 payload = (json.dumps(record, indent=2, separators=(
",",
": ")) +
"\n").encode()
207 temp = path.with_name(f
".{path.name}.tmp.{os.getpid()}")
208 flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | getattr(os,
"O_NOFOLLOW", 0)
209 descriptor = os.open(temp, flags, 0o666)
211 os.fchmod(descriptor, 0o666)
212 remaining = memoryview(payload)
214 remaining = remaining[os.write(descriptor, remaining) :]
219 directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)
224 return path.stat(follow_symlinks=
False)
227def _cleanup(path: Path, identity: os.stat_result |
None) ->
None:
228 """Remove only the exact record inode this broker published."""
232 current = path.stat(follow_symlinks=
False)
233 if (current.st_dev, current.st_ino) != (identity.st_dev, identity.st_ino):
236 directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)
241 except FileNotFoundError:
245@dataclass(frozen=True)
247 """Exact record, lock, and descriptor operations for one finalizer."""
249 cleanup: Callable[[Path, os.stat_result |
None],
None] = _cleanup
250 unlock: Callable[[int, int],
None] = fcntl.flock
251 closer: Callable[[int],
None] = os.close
254def _interrupt(_signum: int, _frame: object) ->
None:
255 """Turn transport-loss signals into finally-based record/lock cleanup."""
256 raise InterruptedError
260 record: tuple[Path, os.stat_result |
None],
262 actions: ReleaseActions |
None =
None,
263) -> OSError | BrokerError |
None:
264 """Attempt record cleanup, unlock, and close while retaining the first error."""
265 path, identity = record
266 active = actions
or ReleaseActions()
267 first_error: OSError | BrokerError |
None =
None
269 active.cleanup(path, identity)
270 except (OSError, BrokerError)
as error:
273 active.unlock(descriptor, fcntl.LOCK_UN)
274 except OSError
as error:
275 if first_error
is None:
277 close_error = _close_error(descriptor, active.closer)
278 if first_error
is None:
279 first_error = close_error
283def hold(request: HoldRequest) -> int:
284 """Acquire, publish the exact process pair, and hold until parent EOF."""
285 if os.getppid() != request.host_pid
or _start_ticks(request.host_pid) != request.host_ticks:
286 msg =
"broker parent is not the authenticated host process"
287 raise BrokerError(msg)
288 fields = _fields(request.fields_b64)
289 descriptor, _identity = _open_lock(request.lock)
290 published: os.stat_result |
None =
None
292 primary: OSError | BrokerError | InterruptedError |
None =
None
295 _take(descriptor, request.wait_s)
296 except BlockingIOError:
299 published = _publish(
301 _record(fields, request.host_pid, request.host_ticks),
303 print(f
"ACQUIRED {os.getpid()} {_start_ticks(os.getpid())}", flush=
True)
304 while os.read(0, 4096):
306 except (OSError, BrokerError, InterruptedError)
as error:
308 cleanup_error = _release_hold((request.record, published), descriptor)
309 if primary
is not None:
310 if cleanup_error
is not None:
311 primary.add_note(f
"broker finalizer also failed: {cleanup_error}")
313 if cleanup_error
is not None:
318def _selftest_fields() -> str:
319 """Return one valid wrapped-hold request for the broker selftest."""
322 "lock_id":
"0123456789abcdef",
323 "holder_class":
"agent",
324 "holder_name":
"selftest",
325 "intent":
"offline broker selftest",
327 "hold_kind":
"wrapped",
328 "break_glass":
"false",
329 "origin":
"selftest",
332 text =
"".join(f
"{key}={value}\n" for key, value
in fields.items())
333 return base64.b64encode(text.encode()).decode(
"ascii")
336def _start_broker(lock: Path, record: Path, host_ticks: int) -> subprocess.Popen[bytes]:
337 """Start these exact source bytes as the synthetic broker child."""
338 source = Path(__file__).read_text(encoding=
"utf-8")
339 return subprocess.Popen(
354 stdin=subprocess.PIPE,
355 stdout=subprocess.PIPE,
356 stderr=subprocess.PIPE,
361def _lock_owners(lock: Path) -> set[int]:
362 """Return kernel FLOCK owner PIDs for one exact inode."""
363 observed = lock.stat()
364 expected = (os.major(observed.st_dev), os.minor(observed.st_dev), observed.st_ino)
365 owners: set[int] = set()
366 for line
in Path(
"/proc/locks").read_text(encoding=
"ascii").splitlines():
367 fields = line.split()
368 for index, field
in enumerate(fields):
369 match = re.fullmatch(
r"([0-9a-fA-F]+):([0-9a-fA-F]+):(\d+)", field)
370 if match
is None or "FLOCK" not in fields
or index == 0:
372 identity = (int(match.group(1), 16), int(match.group(2), 16), int(match.group(3)))
373 if identity == expected
and fields[index - 1].isdigit():
374 owners.add(int(fields[index - 1]))
378def _wait_ack(process: subprocess.Popen[bytes]) -> str:
379 """Read the one flushed acquisition line with a fixed deadline."""
380 if process.stdout
is None:
382 readable, _writable, _exceptional = select.select([process.stdout], [], [], 5)
383 return process.stdout.readline().decode(
"utf-8",
"replace").strip()
if readable
else ""
386def _process_argv(pid: int) -> tuple[bytes, ...] |
None:
387 """Return one live Linux argv only when its NUL framing is canonical."""
389 raw = Path(f
"/proc/{pid}/cmdline").read_bytes()
392 if not raw
or not raw.endswith(b
"\0"):
394 fields = tuple(raw[:-1].split(b
"\0"))
395 return fields
if fields
and all(field
for field
in fields)
else None
398def _start_fd_probe() -> subprocess.Popen[bytes]:
399 """Start one bounded child that stays live until its parent releases it."""
400 return subprocess.Popen(
401 [
"/usr/bin/python3",
"-B",
"-I",
"-S",
"-c", FD_PROBE_SOURCE],
402 stdin=subprocess.PIPE,
403 stdout=subprocess.PIPE,
404 stderr=subprocess.PIPE,
409def _close_process_streams(process: subprocess.Popen[bytes]) -> OSError |
None:
410 """Attempt every parent-side stream close and retain the first failure."""
411 first_error: OSError |
None =
None
412 for stream
in (process.stdin, process.stdout, process.stderr):
417 except OSError
as error:
418 if first_error
is None:
424 process: subprocess.Popen[bytes], *, close_input: bool
425) -> tuple[int, OSError |
None]:
426 """Bound, reap, and close one test child while retaining the first I/O error."""
427 first_error: OSError |
None =
None
428 if close_input
and process.stdin
is not None:
430 process.stdin.close()
431 except OSError
as error:
435 status = process.wait(timeout=5)
436 except subprocess.TimeoutExpired:
438 status = process.wait(timeout=5)
440 close_error = _close_process_streams(process)
441 if first_error
is None:
442 first_error = close_error
443 return status, first_error
446def _stop_fd_probe(process: subprocess.Popen[bytes]) -> int:
447 """Release or forcibly reap the exact direct probe child within a deadline."""
448 if process.stdin
is not None:
449 with suppress(BrokenPipeError):
450 process.stdin.write(b
"x")
451 status, close_error = _finish_child(process, close_input=
True)
452 if close_error
is not None:
458 """Record a parent-side stream close and optionally refuse it."""
460 def __init__(self, *, fail: bool =
False) ->
None:
464 def close(self) -> None:
465 """Record the exact attempt before raising the injected failure."""
468 message =
"injected broker stream-close failure"
469 raise OSError(message)
473 """Expose the three stream fields consumed by exhaustive cleanup."""
475 def __init__(self) -> None:
476 self.stdin = _CloseProbe(fail=
True)
477 self.stdout = _CloseProbe()
478 self.stderr = _CloseProbe()
481def _linked_lock_refusal(root: Path, lock: Path) -> str |
None:
482 """Require a hard-linked lock refusal to close its opened descriptor."""
483 linked = root /
"open-cleanup-linked.lock"
484 os.link(lock, linked)
485 before = {path.name
for path
in Path(
"/proc/self/fd").iterdir()}
489 if {path.name
for path
in Path(
"/proc/self/fd").iterdir()} != before:
490 return "linked lock refusal leaked its opened descriptor"
492 return "multiply-linked canonical lock inode was accepted"
498def _existing_lock_open_selftest(root: Path) -> str |
None:
499 """Model protected_regular and require a no-create open."""
500 lock = root /
"open-existing.lock"
501 lock.write_bytes(b
"")
502 calls: list[int] = []
504 def protected_opener(path: Path, flags: int, mode: int) -> int:
506 if flags & os.O_CREAT:
507 message =
"injected fs.protected_regular refusal"
508 raise PermissionError(message)
509 return os.open(path, flags, mode)
511 descriptor: int |
None =
None
513 descriptor, _identity = _open_lock(lock, opener=protected_opener)
515 return "existing canonical lock failed under protected_regular semantics"
517 if descriptor
is not None:
519 if len(calls) != 1
or calls[0] & (os.O_CREAT | os.O_EXCL):
520 return "existing canonical lock was reopened with creation flags"
524def _missing_lock_creation_selftest(root: Path) -> str |
None:
525 """Require one no-create open followed by one exclusive create."""
526 lock = root /
"open-missing.lock"
527 calls: list[int] = []
529 def opener(path: Path, flags: int, mode: int) -> int:
531 return os.open(path, flags, mode)
533 descriptor: int |
None =
None
535 descriptor, _identity = _open_lock(lock, opener=opener)
537 return "missing canonical lock was not created"
539 if descriptor
is not None:
541 create_flags = os.O_CREAT | os.O_EXCL
544 len(calls) != expected_calls
545 or calls[0] & create_flags
546 or calls[1] & create_flags != create_flags
548 return "missing canonical lock did not use one exclusive-create retry"
552def _concurrent_lock_creation_selftest(root: Path) -> str |
None:
553 """Require a no-create reopen after another creator wins the race."""
554 lock = root /
"open-collision.lock"
555 calls: list[int] = []
558 def opener(path: Path, flags: int, mode: int) -> int:
561 raise FileNotFoundError(path)
562 if len(calls) == exclusive_call:
563 path.write_bytes(b
"")
564 raise FileExistsError(path)
565 return os.open(path, flags, mode)
567 descriptor: int |
None =
None
569 descriptor, _identity = _open_lock(lock, opener=opener)
571 return "concurrent canonical lock creation was not accepted"
573 if descriptor
is not None:
575 create_flags = os.O_CREAT | os.O_EXCL
578 len(calls) != expected_calls
579 or calls[0] & create_flags
580 or calls[1] & create_flags != create_flags
581 or calls[2] & create_flags
583 return "concurrent canonical lock creation did not reopen safely"
587def _open_lock_error_selftest(root: Path) -> list[str]:
588 """Require unexpected open and create failures to remain exact."""
589 failures: list[str] = []
590 denied_calls: list[int] = []
592 def denied_opener(_path: Path, flags: int, _mode: int) -> int:
593 denied_calls.append(flags)
594 message =
"injected ordinary-open refusal"
595 raise PermissionError(message)
598 _open_lock(root /
"open-denied.lock", opener=denied_opener)
599 except BrokerError
as error:
600 if not isinstance(error.__cause__, PermissionError)
or len(denied_calls) != 1:
601 failures.append(
"unexpected ordinary-open failure was retried or obscured")
603 failures.append(
"unexpected ordinary-open failure was accepted")
605 creation_calls: list[int] = []
606 creation_failure = OSError(
"injected exclusive-create failure")
607 expected_creation_calls = 2
609 def creation_opener(_path: Path, flags: int, _mode: int) -> int:
610 creation_calls.append(flags)
611 if len(creation_calls) == 1:
612 raise FileNotFoundError
613 raise creation_failure
616 _open_lock(root /
"open-create-error.lock", opener=creation_opener)
617 except BrokerError
as error:
619 error.__cause__
is not creation_failure
620 or len(creation_calls) != expected_creation_calls
622 failures.append(
"unexpected exclusive-create failure was retried or obscured")
624 failures.append(
"unexpected exclusive-create failure was accepted")
628def _open_lock_creation_selftest(root: Path) -> list[str]:
629 """Exercise existing, missing, raced, and failed canonical opens."""
630 failures = _open_lock_error_selftest(root)
632 _existing_lock_open_selftest,
633 _missing_lock_creation_selftest,
634 _concurrent_lock_creation_selftest,
636 failure =
check(root)
637 if failure
is not None:
638 failures.append(failure)
642def _open_lock_cleanup_selftest(root: Path) -> list[str]:
643 """Prove every post-open refusal releases exactly its owned lock FD."""
644 failures: list[str] = []
645 lock = root /
"open-cleanup.lock"
646 lock.write_bytes(b
"")
649 close_attempts: list[int] = []
651 def opener(path: Path, flags: int, mode: int) -> int:
653 opened = os.open(path, flags, mode)
656 def deny_path(_path: Path) -> os.stat_result:
657 message =
"injected canonical-path stat failure"
658 raise PermissionError(message)
660 def close_reuse(descriptor: int) ->
None:
662 close_attempts.append(descriptor)
664 replacement = os.open(os.devnull, os.O_RDONLY | os.O_CLOEXEC)
665 if replacement != descriptor:
666 message =
"lock descriptor number was not reused"
667 raise BrokerError(message)
668 message =
"injected ambiguous lock close failure"
669 raise OSError(message)
672 _open_lock(lock, opener=opener, path_stat=deny_path, closer=close_reuse)
673 except BrokerError
as error:
675 error.__cause__
is None
676 or str(error.__cause__) !=
"injected canonical-path stat failure"
677 or close_attempts != [opened]
678 or replacement != opened
680 failures.append(
"post-open stat refusal lost its primary error or retried close")
683 os.fstat(replacement)
685 failures.append(
"post-open stat refusal closed a reused unrelated descriptor")
687 failures.append(
"post-open canonical-path stat failure was accepted")
689 with suppress(OSError):
690 os.close(replacement)
692 linked_failure = _linked_lock_refusal(root, lock)
693 if linked_failure
is not None:
694 failures.append(linked_failure)
698def _finalizer_cleanup_selftest(root: Path) -> list[str]:
699 """Prove cleanup, unlock, close, and stream failures remain exhaustive."""
700 failures: list[str] = []
701 calls: list[str] = []
703 def fail_cleanup(_path: Path, _identity: os.stat_result |
None) ->
None:
704 calls.append(
"cleanup")
705 message =
"injected record cleanup failure"
706 raise BrokerError(message)
708 def fail_unlock(_descriptor: int, _operation: int) ->
None:
709 calls.append(
"unlock")
710 message =
"injected unlock failure"
711 raise OSError(message)
713 def fail_close(_descriptor: int) ->
None:
714 calls.append(
"close")
715 message =
"injected close failure"
716 raise OSError(message)
718 error = _release_hold(
719 (root /
"unused.json",
None),
721 ReleaseActions(fail_cleanup, fail_unlock, fail_close),
723 if not isinstance(error, BrokerError)
or calls != [
"cleanup",
"unlock",
"close"]:
724 failures.append(
"hold finalizer did not attempt every action or retain its first error")
725 process = _ProcessProbe()
726 stream_error = _close_process_streams(process)
727 streams = (process.stdin, process.stdout, process.stderr)
728 if stream_error
is None or not all(stream.closed
for stream
in streams):
729 failures.append(
"parent stream cleanup did not attempt every close after failure")
733def _fd_probe_selftest(lock: Path) -> list[str]:
734 """Prove a ready, parent-held child has not inherited the broker lock."""
736 child = _start_fd_probe()
738 if _wait_ack(child) !=
"READY" or child.poll()
is not None:
739 failures.append(
"descriptor probe did not remain live after its readiness receipt")
747 FD_PROBE_SOURCE.encode(
"ascii"),
749 if _process_argv(child.pid) != expected_argv:
750 failures.append(
"descriptor probe did not retain its fixed reviewed argv")
751 lock_identity = lock.stat()
753 descriptors = tuple(Path(f
"/proc/{child.pid}/fd").iterdir())
754 for fd
in descriptors:
756 if (observed.st_dev, observed.st_ino) == (
757 lock_identity.st_dev,
758 lock_identity.st_ino,
760 failures.append(
"broker lock descriptor leaked to an unrelated child")
763 failures.append(
"live descriptor probe disappeared during its inode proof")
765 if _stop_fd_probe(child) != 0:
766 failures.append(
"descriptor probe did not exit cleanly after release")
770def _broker_lifecycle_selftest(root: Path) -> list[str]:
771 """Prove kernel PID, competitor exclusion, cleanup, and fd non-inheritance."""
772 failures: list[str] = []
773 lock = root /
"board.lock"
774 record = root /
"holder.json"
775 lock.write_bytes(b
"")
776 host_ticks = _start_ticks(os.getpid())
777 process = _start_broker(lock, record, host_ticks)
779 ack = _wait_ack(process)
780 source = Path(__file__).read_bytes()
791 _selftest_fields().encode(
"ascii"),
792 str(os.getpid()).encode(
"ascii"),
793 str(host_ticks).encode(
"ascii"),
795 argv = _process_argv(process.pid)
796 if argv != expected_argv:
797 failures.append(
"broker child did not retain its fixed reviewed argv")
798 if ack.split()[:2] != [
"ACQUIRED", str(process.pid)]
or _lock_owners(lock) != {process.pid}:
799 failures.append(
"broker acquisition lacks one matching kernel FLOCK owner")
800 competitor = os.open(lock, os.O_RDWR | os.O_CLOEXEC)
803 fcntl.flock(competitor, fcntl.LOCK_EX | fcntl.LOCK_NB)
804 except BlockingIOError:
807 failures.append(
"competing client acquired the broker-held inode")
810 failures.extend(_fd_probe_selftest(lock))
812 status, close_error = _finish_child(process, close_input=
True)
813 if status != 0
or record.exists()
or _lock_owners(lock):
814 failures.append(
"stdin EOF did not release and clean the broker hold")
815 if close_error
is not None:
816 failures.append(
"broker lifecycle did not close every parent-side stream")
820def _broker_failure_selftest(root: Path) -> list[str]:
821 """Prove signal, stale-parent, and replaced-record cleanup fail closed."""
822 failures: list[str] = []
823 lock = root /
"signal.lock"
824 record = root /
"signal.json"
825 lock.write_bytes(b
"")
826 process = _start_broker(lock, record, _start_ticks(os.getpid()))
828 if not _wait_ack(process):
829 failures.append(
"signal fixture did not acquire")
830 replacement = record.with_suffix(
".replacement")
831 replacement.write_text(
"unowned\n", encoding=
"utf-8")
832 replacement.replace(record)
833 process.send_signal(signal.SIGTERM)
835 _status, close_error = _finish_child(process, close_input=
False)
836 if record.read_text(encoding=
"utf-8") !=
"unowned\n":
837 failures.append(
"broker cleanup removed a replaced holder record")
838 if close_error
is not None:
839 failures.append(
"signaled broker did not close every parent-side stream")
840 stale = _start_broker(root /
"stale.lock", root /
"stale.json", 1)
841 stale_status, stale_close_error = _finish_child(stale, close_input=
True)
842 if stale_status == 0:
843 failures.append(
"stale parent start time was accepted")
844 if stale_close_error
is not None:
845 failures.append(
"stale-parent broker did not close every parent-side stream")
849def run_selftest() -> list[str]:
850 """Exercise the real Linux broker process without a bench or network."""
851 if not Path(
"/proc/locks").is_file():
852 return [
"bench lock broker requires Linux /proc/locks"]
853 with tempfile.TemporaryDirectory(prefix=
"ra8-lock-broker-")
as raw:
856 _open_lock_creation_selftest(root)
857 + _open_lock_cleanup_selftest(root)
858 + _finalizer_cleanup_selftest(root)
859 + _broker_lifecycle_selftest(root)
860 + _broker_failure_selftest(root)
865 """Parse one fixed broker request and fail closed with stable exit codes."""
866 if sys.argv[1:] == [
"--selftest"]:
867 failures = run_selftest()
868 for failure
in failures:
869 print(f
"bench-lock-broker selftest: {failure}", file=sys.stderr)
870 return 1
if failures
else 0
871 parser = argparse.ArgumentParser(description=__doc__)
872 parser.add_argument(
"lock", type=Path)
873 parser.add_argument(
"record", type=Path)
874 parser.add_argument(
"wait_s", type=int)
875 parser.add_argument(
"fields_b64")
876 parser.add_argument(
"host_pid", type=int)
877 parser.add_argument(
"host_start_ticks", type=int)
878 args = parser.parse_args()
879 for signum
in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
880 signal.signal(signum, _interrupt)
889 args.host_start_ticks,
892 except InterruptedError:
894 except (BrokerError, OSError)
as exc:
895 print(f
"bench-lock-broker: {exc}", file=sys.stderr)
899if __name__ ==
"__main__":
900 raise SystemExit(
main())
void main(void)
The application entry point Reset_Handler hands control to.