103 lines
3.4 KiB
Python
103 lines
3.4 KiB
Python
"""Atomic local IPC snapshots; no network access in the simulation process."""
|
|
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
class StateChannel:
|
|
"""Latest-value local IPC using SQLite WAL, available in both Python hosts.
|
|
|
|
Readers never hold Windows rename/delete locks on a telemetry snapshot.
|
|
The durable camera/motion journals remain separate evidence artifacts.
|
|
Each process owns its connection; only small control/telemetry JSON enters it.
|
|
"""
|
|
|
|
def __init__(self, directory: Path):
|
|
self.connection = sqlite3.connect(directory / "channel.sqlite3", timeout=0.1)
|
|
self.connection.execute("PRAGMA journal_mode=WAL")
|
|
self.connection.execute("PRAGMA synchronous=NORMAL")
|
|
self.connection.execute(
|
|
"CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT NOT NULL)"
|
|
)
|
|
self.connection.commit()
|
|
|
|
def write(self, key, value):
|
|
encoded = json.dumps(value, allow_nan=False)
|
|
with self.connection:
|
|
self.connection.execute(
|
|
"INSERT INTO state(key,value) VALUES(?,?) "
|
|
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
(key, encoded),
|
|
)
|
|
|
|
def read(self, key, default=None):
|
|
row = self.connection.execute("SELECT value FROM state WHERE key=?", (key,)).fetchone()
|
|
return default if row is None else json.loads(row[0])
|
|
|
|
def close(self):
|
|
self.connection.close()
|
|
|
|
|
|
def sharing_retry(operation):
|
|
# A concurrent Windows reader briefly denies delete/replace sharing. Retry
|
|
# only this local file operation, bounded to 14ms; permanent errors surface.
|
|
for attempt in range(8):
|
|
try:
|
|
return operation()
|
|
except PermissionError:
|
|
if attempt == 7:
|
|
raise
|
|
time.sleep(0.002)
|
|
|
|
|
|
def write_json(path: Path, value):
|
|
temporary = path.with_name(path.name + f".{os.getpid()}.tmp")
|
|
temporary.write_text(json.dumps(value, allow_nan=False), encoding="utf-8")
|
|
sharing_retry(lambda: os.replace(temporary, path))
|
|
|
|
|
|
def read_json(path: Path, default=None):
|
|
try:
|
|
return json.loads(sharing_retry(lambda: read_shared_text(path)))
|
|
except FileNotFoundError:
|
|
return default
|
|
|
|
|
|
def read_shared_text(path: Path):
|
|
if os.name != "nt":
|
|
return path.read_text(encoding="utf-8")
|
|
# Python's normal Windows open does not grant FILE_SHARE_DELETE. An atomic
|
|
# writer must remain able to replace a snapshot while a reader holds it.
|
|
import ctypes
|
|
import msvcrt
|
|
from ctypes import wintypes
|
|
|
|
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
kernel.CreateFileW.argtypes = [
|
|
wintypes.LPCWSTR,
|
|
wintypes.DWORD,
|
|
wintypes.DWORD,
|
|
ctypes.c_void_p,
|
|
wintypes.DWORD,
|
|
wintypes.DWORD,
|
|
wintypes.HANDLE,
|
|
]
|
|
kernel.CreateFileW.restype = wintypes.HANDLE
|
|
handle = kernel.CreateFileW(str(path), 0x80000000, 7, None, 3, 0x80, None)
|
|
if handle == wintypes.HANDLE(-1).value:
|
|
code = ctypes.get_last_error()
|
|
if code in (2, 3):
|
|
raise FileNotFoundError(str(path))
|
|
raise ctypes.WinError(code)
|
|
try:
|
|
descriptor = msvcrt.open_osfhandle(handle, os.O_RDONLY)
|
|
except BaseException:
|
|
kernel.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
kernel.CloseHandle(handle)
|
|
raise
|
|
with os.fdopen(descriptor, "r", encoding="utf-8") as stream:
|
|
return stream.read()
|