85 lines
3.6 KiB
Python
85 lines
3.6 KiB
Python
"""Один владелец файлового backend; timeout инвалидирует канал mutations."""
|
|
from __future__ import annotations
|
|
import fcntl
|
|
import json
|
|
from pathlib import Path
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from .build import write_json
|
|
|
|
|
|
class BridgeError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class FileBridge:
|
|
def __init__(self, directory, session, timeout=5):
|
|
self.directory = Path(directory)
|
|
self.session = session
|
|
self.timeout = timeout
|
|
self.generation = None
|
|
self.invalid = False
|
|
self._mutex = threading.Lock()
|
|
self._owner = (self.directory/'owner.lock').open('a')
|
|
try:
|
|
fcntl.flock(self._owner, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except OSError:
|
|
self._owner.close()
|
|
raise BridgeError('У backend уже есть управляющая сессия')
|
|
|
|
def close(self):
|
|
with self._mutex:
|
|
self._owner.close()
|
|
|
|
def request(self, command, **args):
|
|
with self._mutex:
|
|
if self._owner.closed:
|
|
raise BridgeError('Канал закрыт')
|
|
if self.invalid:
|
|
raise BridgeError('Канал инвалидирован после timeout; требуется новая сессия')
|
|
identity = str(uuid.uuid4().int)
|
|
request = self.directory/f'req_{identity}.json'
|
|
response = self.directory/f'resp_{identity}.json'
|
|
write_json(request, {'session': self.session, 'generation': self.generation,
|
|
'command': command, 'args': args})
|
|
deadline = time.monotonic()+self.timeout
|
|
while time.monotonic() < deadline:
|
|
if response.exists():
|
|
try:
|
|
value = json.loads(response.read_text())
|
|
if not isinstance(value, dict) or not {'session', 'generation', 'ok'} <= value.keys():
|
|
raise ValueError('Неверная структура ответа')
|
|
except (OSError, ValueError) as error:
|
|
self.invalid = True
|
|
raise BridgeError('Повреждённый ответ backend: ' + str(error)) from error
|
|
response.unlink()
|
|
if value['session'] != self.session:
|
|
self.invalid = True
|
|
raise BridgeError('Ответ от другой сессии')
|
|
self.generation = value['generation']
|
|
if not value['ok']:
|
|
raise BridgeError(value['error'])
|
|
return value['result']
|
|
time.sleep(.005)
|
|
self.invalid = True
|
|
request.unlink(missing_ok=True)
|
|
raise BridgeError('Timeout: результат команды неизвестен; автоматический повтор запрещён')
|
|
|
|
def wait_stopped(self, timeout=5):
|
|
deadline = time.monotonic()+timeout
|
|
while time.monotonic() < deadline:
|
|
snapshot = self.request('snapshot')
|
|
if snapshot['state']=='stopped':
|
|
return snapshot
|
|
if snapshot['state']=='invalidated':
|
|
raise BridgeError('Сессия MAME инвалидирована reset/load')
|
|
time.sleep(.005)
|
|
raise BridgeError('CPU не остановился за отведённое время')
|
|
|
|
def handshake(self):
|
|
response = self.request('hello')
|
|
if response['protocol'] != 1:
|
|
raise BridgeError('Неподдержанная версия протокола')
|
|
return response
|