Verify visible hello MCP run and add shared control lease

This commit is contained in:
Александр Петров
2026-09-17 18:15:01 +03:00
parent 619c4c9af5
commit a43e7bda89
14 changed files with 575 additions and 44 deletions
+105 -5
View File
@@ -9,6 +9,7 @@ import socketserver
import string
import threading
import time
import uuid
from .session import DebugSession, SessionError
from .transport import BridgeError
@@ -16,6 +17,8 @@ from .macros import validate_log_message
class SessionController:
CONTROL_TTL = 30.0
def __init__(self, session: DebugSession):
self.session = session
self.lock = threading.RLock()
@@ -26,6 +29,11 @@ class SessionController:
self.running = False
self.source_step = None
self.closed = False
self.session_id = uuid.uuid4().hex
self.control_owner: str | None = None
self.control_deadline = 0.0
self.owner_deadlines: dict[str, float] = {}
self.held_inputs: dict[str, set[tuple[str, int]]] = {}
self.source_breakpoints: dict[str, list[int]] = {}
self.function_breakpoints: list[int] = []
self.breakpoint_info: dict[int, dict] = {}
@@ -84,6 +92,7 @@ class SessionController:
snapshot = self.session.bridge.request('snapshot')
if snapshot['state'] == 'stopped':
if self.running:
self._release_all_inputs()
location = self.session.where(snapshot)
if self.source_step is not None:
self._source_step_stopped(location)
@@ -104,6 +113,7 @@ class SessionController:
self._emit('invalidated', {'reason': 'reset_or_load'})
if not self.running:
next_idle_snapshot = time.monotonic() + .5
self._reap_owners()
except (BridgeError, SessionError, ValueError, OSError) as error:
self.running = False
self.source_step = None
@@ -114,6 +124,11 @@ class SessionController:
def close(self) -> None:
self.closed = True
self.poller.join(timeout=1)
with self.lock:
try:
self._release_all_inputs()
except (BridgeError, SessionError, OSError):
pass
self.session.bridge.close()
@staticmethod
@@ -123,8 +138,59 @@ class SessionController:
raise SessionError('owner должен быть непустой строкой до 128 символов')
return owner
def _control(self, arguments: dict) -> dict:
"""Захватить или продлить управление; чужой живой lease не отбирать."""
owner = self._owner(arguments)
self._touch_owner(owner)
now = time.monotonic()
if self.control_owner is not None and self.control_deadline <= now:
self.control_owner = None
if self.control_owner not in (None, owner):
raise SessionError(f'CPU управляет {self.control_owner}; дождитесь release или истечения lease')
if self.control_owner != owner:
self.control_owner = owner
self._emit('control', {'owner': owner})
self.control_deadline = now + self.CONTROL_TTL
return {'owner': owner, 'expires_in': self.CONTROL_TTL}
def _touch_owner(self, owner: str) -> None:
if owner.startswith('mcp:'):
self.owner_deadlines[owner] = time.monotonic() + self.CONTROL_TTL
def _release_inputs(self, owner: str) -> None:
for tag, mask in self.held_inputs.pop(owner, set()):
self.session.bridge.request('key', tag=tag, mask=mask, down=False)
def _release_all_inputs(self) -> None:
for owner in list(self.held_inputs):
self._release_inputs(owner)
def _reap_owners(self) -> None:
now = time.monotonic()
for owner, deadline in list(self.owner_deadlines.items()):
if deadline > now:
continue
self._release_inputs(owner)
identifiers = [identifier for identifier, info in self.breakpoint_info.items()
if info.get('owner') == owner]
for identifier in identifiers:
self.session.clear_breakpoint(identifier)
self.breakpoint_info.pop(identifier, None)
if self.control_owner == owner:
self.control_owner = None
self.control_deadline = 0.0
self._emit('control', {'owner': None})
self.owner_deadlines.pop(owner, None)
self._emit('owner_expired', {'owner': owner, 'cleared_breakpoints': identifiers})
def call(self, method: str, arguments: dict) -> object:
if method == 'events':
owner = arguments.get('owner')
if owner is not None:
with self.lock:
if self.control_owner == self._owner(arguments) and \
self.control_deadline > time.monotonic():
self.control_deadline = time.monotonic() + self.CONTROL_TTL
after = int(arguments.get('after', 0))
timeout = min(max(float(arguments.get('timeout', 0)), 0), 30)
deadline = time.monotonic() + timeout
@@ -138,12 +204,32 @@ class SessionController:
with self.lock:
if self.closed:
raise SessionError('Сессия закрыта или инвалидирована')
if method == 'claim_control':
return self._control(arguments)
if method == 'renew_control':
owner = self._owner(arguments)
self._touch_owner(owner)
if self.control_owner == owner and self.control_deadline > time.monotonic():
self.control_deadline = time.monotonic() + self.CONTROL_TTL
return {'owner': owner, 'has_control': True,
'expires_in': self.CONTROL_TTL}
return {'owner': owner, 'has_control': False}
if method == 'release_control':
owner = self._owner(arguments)
self._release_inputs(owner)
if self.control_owner == owner:
self.control_owner = None
self.control_deadline = 0.0
self._emit('control', {'owner': None})
return {'released': owner}
if self.running and method in ('where', 'registers', 'read_memory',
'read_variable', 'break_line', 'break_function'):
raise SessionError('Операция требует остановленного CPU; сначала Pause')
if method == 'status':
return {**self.attached, 'running': self.running,
'event_sequence': self.sequence}
'event_sequence': self.sequence, 'session_id': self.session_id,
'control_owner': self.control_owner if
self.control_deadline > time.monotonic() else None}
if method == 'snapshot':
return self.session.bridge.request('snapshot')
if method == 'mame_console_tail':
@@ -152,9 +238,16 @@ class SessionController:
if method == 'input_key':
if not self.running and arguments['down'] is not False:
raise SessionError('Нажатие возможно только при running CPU')
return self.session.bridge.request(
'key', tag=arguments['tag'], mask=int(arguments['mask']),
down=arguments['down'])
self._control(arguments)
tag, mask = arguments['tag'], int(arguments['mask'])
result = self.session.bridge.request('key', tag=tag, mask=mask,
down=arguments['down'])
held = self.held_inputs.setdefault(self._owner(arguments), set())
if arguments['down']:
held.add((tag, mask))
else:
held.discard((tag, mask))
return result
if method == 'where':
return self.session.where()
if method == 'registers':
@@ -177,6 +270,7 @@ class SessionController:
return self.session.read_variable(arguments['name'], arguments.get('module'))
if method == 'break_line':
owner = self._owner(arguments)
self._touch_owner(owner)
item = self.session.break_line(arguments['file'], int(arguments['line']))
self.breakpoint_info[item['id']] = {
'kind': 'stop', 'message': None, 'locations': item['locations'],
@@ -184,6 +278,7 @@ class SessionController:
return item
if method == 'break_function':
owner = self._owner(arguments)
self._touch_owner(owner)
item = self.session.break_function(arguments['name'])
self.breakpoint_info[item['id']] = {
'kind': 'stop', 'message': None, 'locations': item['locations'],
@@ -192,7 +287,8 @@ class SessionController:
if method == 'clear_breakpoint':
identifier = int(arguments['id'])
owner = arguments.get('owner')
if owner is not None and self.breakpoint_info.get(identifier, {}).get('owner') != owner:
if identifier in self.breakpoint_info and owner is not None and \
self.breakpoint_info[identifier].get('owner') != owner:
raise SessionError('Точка не принадлежит этому клиенту')
result = self.session.clear_breakpoint(identifier)
self.breakpoint_info.pop(identifier, None)
@@ -217,11 +313,13 @@ class SessionController:
if method == 'continue':
if self.running:
raise SessionError('CPU уже выполняется; сначала Pause')
self._control(arguments)
result = self.session.bridge.request('continue')
self.running = True
self._emit('continued', {})
return result
if method == 'pause':
self._control(arguments)
self.source_step = None
self.session.bridge.request('pause')
location = self.session.where(self.session.bridge.wait_stopped())
@@ -231,6 +329,7 @@ class SessionController:
if method == 'step':
if self.running:
raise SessionError('CPU уже выполняется; сначала Pause')
self._control(arguments)
self.session.bridge.request('step')
self.running = True
self._emit('continued', {'reason': 'step'})
@@ -239,6 +338,7 @@ class SessionController:
self._emit('stopped', {'reason': 'step', 'location': location})
return location
if method == 'source_step':
self._control(arguments)
return self._source_step(arguments.get('kind', 'into'))
raise SessionError('Неизвестный RPC-метод: ' + method)